From 5ed7e819e8ba866e0c8eff4ebcdd78737ca8d5d0 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 5 Sep 2023 17:23:58 +0800 Subject: [PATCH] fix label constraint --- packages/api/src/entity/highlight.ts | 2 +- packages/api/src/entity/label.ts | 4 + packages/api/src/entity/library_item.ts | 2 +- .../api/src/entity/library_item_preview.ts | 43 ------- packages/api/src/events/entity_created.ts | 2 +- packages/api/src/generated/graphql.ts | 52 ++++---- packages/api/src/generated/schema.graphql | 26 ++-- packages/api/src/repository/highlight.ts | 7 +- packages/api/src/resolvers/article/index.ts | 116 +++++++----------- packages/api/src/resolvers/highlight/index.ts | 18 ++- packages/api/src/resolvers/labels/index.ts | 3 +- packages/api/src/schema.ts | 26 ++-- packages/api/src/services/highlights.ts | 46 +++++-- packages/api/src/services/labels.ts | 6 +- packages/api/src/services/library_item.ts | 2 + packages/api/src/services/save_page.ts | 4 +- packages/api/src/utils/helpers.ts | 13 +- .../db/migrations/0118.do.library_item.sql | 2 +- .../db/migrations/0119.do.entity_labels.sql | 8 +- .../migrations/0120.do.alter_labels_table.sql | 17 +++ .../0120.do.library_item_preview.sql | 23 ---- .../0120.undo.alter_labels_table.sql | 16 +++ .../0120.undo.library_item_preview.sql | 11 -- .../migrations/0121.do.update_highlight.sql | 2 +- packages/db/migrations/0123.do.add_rls.sql | 5 +- packages/db/migrations/0123.undo.add_rls.sql | 5 - yarn.lock | 6 +- 27 files changed, 227 insertions(+), 240 deletions(-) delete mode 100644 packages/api/src/entity/library_item_preview.ts create mode 100755 packages/db/migrations/0120.do.alter_labels_table.sql delete mode 100755 packages/db/migrations/0120.do.library_item_preview.sql create mode 100755 packages/db/migrations/0120.undo.alter_labels_table.sql delete mode 100755 packages/db/migrations/0120.undo.library_item_preview.sql diff --git a/packages/api/src/entity/highlight.ts b/packages/api/src/entity/highlight.ts index a2697a3e1..f1044dc7b 100644 --- a/packages/api/src/entity/highlight.ts +++ b/packages/api/src/entity/highlight.ts @@ -57,7 +57,7 @@ export class Highlight { createdAt!: Date @UpdateDateColumn() - updatedAt!: Date + updatedAt?: Date | null @Column('timestamp') sharedAt?: Date diff --git a/packages/api/src/entity/label.ts b/packages/api/src/entity/label.ts index dd0c65079..7296e85bf 100644 --- a/packages/api/src/entity/label.ts +++ b/packages/api/src/entity/label.ts @@ -5,6 +5,7 @@ import { JoinColumn, ManyToOne, PrimaryGeneratedColumn, + UpdateDateColumn, } from 'typeorm' import { User } from './user' @@ -34,4 +35,7 @@ export class Label { @Column('boolean', { default: false }) internal!: boolean + + @UpdateDateColumn() + updatedAt?: Date | null } diff --git a/packages/api/src/entity/library_item.ts b/packages/api/src/entity/library_item.ts index 4399100e5..597d8f4c8 100644 --- a/packages/api/src/entity/library_item.ts +++ b/packages/api/src/entity/library_item.ts @@ -103,7 +103,7 @@ export class LibraryItem { readAt?: Date | null @UpdateDateColumn() - updatedAt!: Date + updatedAt?: Date | null @Column('text', { nullable: true }) itemLanguage?: string | null diff --git a/packages/api/src/entity/library_item_preview.ts b/packages/api/src/entity/library_item_preview.ts deleted file mode 100644 index 21e97d35a..000000000 --- a/packages/api/src/entity/library_item_preview.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - JoinColumn, - OneToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm' -import { LibraryItem } from './library_item' -import { User } from './user' - -@Entity({ name: 'library_item_preview' }) -export class LibraryItemPreview { - @PrimaryGeneratedColumn('uuid') - id?: string - - @OneToOne(() => User, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'sender_id' }) - sender!: User - - @Column('text', { name: 'recipient_ids', array: true }) - recipientIds!: string[] - - @OneToOne(() => LibraryItem, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'library_item_id' }) - libraryItem!: LibraryItem - - @Column('text') - thumbnail?: string - - @Column('bool', { default: false }) - includesNote?: boolean - - @Column('bool', { default: false }) - includesHighlight?: boolean - - @CreateDateColumn() - createdAt?: Date - - @UpdateDateColumn() - updatedAt?: Date -} diff --git a/packages/api/src/events/entity_created.ts b/packages/api/src/events/entity_created.ts index 7c2bb4e78..a5e08a2f7 100644 --- a/packages/api/src/events/entity_created.ts +++ b/packages/api/src/events/entity_created.ts @@ -19,7 +19,7 @@ export class PublishEntitySubscriber implements EntitySubscriberInterface { const msg = JSON.stringify({ type: 'EntityCreated', entity: event.entity, - entityClass: event.entity.constructor.name, + entityClass: event.entity?.constructor?.name, }) if (env.dev.isLocal) { diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 19f11732e..b72113048 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -124,7 +124,7 @@ export type Article = { title: Scalars['String']; unsubHttpUrl?: Maybe; unsubMailTo?: Maybe; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; uploadFileId?: Maybe; url: Scalars['String']; wordsCount?: Maybe; @@ -167,7 +167,7 @@ export type ArticleSavingRequest = { id: Scalars['ID']; slug: Scalars['String']; status: ArticleSavingRequestStatus; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; url: Scalars['String']; user: User; /** @deprecated userId has been replaced with user */ @@ -721,7 +721,7 @@ export type Feature = { id: Scalars['ID']; name: Scalars['String']; token: Scalars['String']; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; }; export type FeedArticle = { @@ -771,7 +771,7 @@ export type Filter = { id: Scalars['ID']; name: Scalars['String']; position: Scalars['Int']; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; visible?: Maybe; }; @@ -928,7 +928,7 @@ export type Highlight = { shortId: Scalars['String']; suffix?: Maybe; type: HighlightType; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; user: User; }; @@ -938,7 +938,7 @@ export type HighlightReply = { highlight: Highlight; id: Scalars['ID']; text: Scalars['String']; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; user: User; }; @@ -979,7 +979,7 @@ export type Integration = { taskName?: Maybe; token: Scalars['String']; type: IntegrationType; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; }; export enum IntegrationType { @@ -1082,7 +1082,7 @@ export type Link = { shareInfo: LinkShareInfo; shareStats: ShareStats; slug: Scalars['String']; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; url: Scalars['String']; }; @@ -1646,7 +1646,7 @@ export type Page = { readableHtml: Scalars['String']; title: Scalars['String']; type: PageType; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; url: Scalars['String']; }; @@ -1953,7 +1953,7 @@ export type RecommendationGroup = { members: Array; name: Scalars['String']; topics?: Maybe>; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; }; export type RecommendingUser = { @@ -2037,7 +2037,7 @@ export type Rule = { filter: Scalars['String']; id: Scalars['ID']; name: Scalars['String']; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; }; export type RuleAction = { @@ -2642,7 +2642,7 @@ export type Subscription = { type: SubscriptionType; unsubscribeHttpUrl?: Maybe; unsubscribeMailTo?: Maybe; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; url?: Maybe; }; @@ -3191,7 +3191,7 @@ export type Webhook = { eventTypes: Array; id: Scalars['ID']; method: Scalars['String']; - updatedAt: Scalars['Date']; + updatedAt?: Maybe; url: Scalars['String']; }; @@ -4314,7 +4314,7 @@ export type ArticleResolvers; unsubHttpUrl?: Resolver, ParentType, ContextType>; unsubMailTo?: Resolver, ParentType, ContextType>; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; uploadFileId?: Resolver, ParentType, ContextType>; url?: Resolver; wordsCount?: Resolver, ParentType, ContextType>; @@ -4343,7 +4343,7 @@ export type ArticleSavingRequestResolvers; slug?: Resolver; status?: Resolver; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; url?: Resolver; user?: Resolver; userId?: Resolver; @@ -4712,7 +4712,7 @@ export type FeatureResolvers; name?: Resolver; token?: Resolver; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -4760,7 +4760,7 @@ export type FilterResolvers; name?: Resolver; position?: Resolver; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; visible?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -4882,7 +4882,7 @@ export type HighlightResolvers; suffix?: Resolver, ParentType, ContextType>; type?: Resolver; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; user?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -4892,7 +4892,7 @@ export type HighlightReplyResolvers; id?: Resolver; text?: Resolver; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; user?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -4924,7 +4924,7 @@ export type IntegrationResolvers, ParentType, ContextType>; token?: Resolver; type?: Resolver; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -5007,7 +5007,7 @@ export type LinkResolvers; shareStats?: Resolver; slug?: Resolver; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; url?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -5216,7 +5216,7 @@ export type PageResolvers; title?: Resolver; type?: Resolver; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; url?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -5379,7 +5379,7 @@ export type RecommendationGroupResolvers, ParentType, ContextType>; name?: Resolver; topics?: Resolver>, ParentType, ContextType>; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -5440,7 +5440,7 @@ export type RuleResolvers; id?: Resolver; name?: Resolver; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -5790,7 +5790,7 @@ export type SubscriptionResolvers; unsubscribeHttpUrl?: SubscriptionResolver, "unsubscribeHttpUrl", ParentType, ContextType>; unsubscribeMailTo?: SubscriptionResolver, "unsubscribeMailTo", ParentType, ContextType>; - updatedAt?: SubscriptionResolver; + updatedAt?: SubscriptionResolver, "updatedAt", ParentType, ContextType>; url?: SubscriptionResolver, "url", ParentType, ContextType>; }; @@ -6138,7 +6138,7 @@ export type WebhookResolvers, ParentType, ContextType>; id?: Resolver; method?: Resolver; - updatedAt?: Resolver; + updatedAt?: Resolver, ParentType, ContextType>; url?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index dcaa201f1..b70838275 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -100,7 +100,7 @@ type Article { title: String! unsubHttpUrl: String unsubMailTo: String - updatedAt: Date! + updatedAt: Date uploadFileId: ID url: String! wordsCount: Int @@ -134,7 +134,7 @@ type ArticleSavingRequest { id: ID! slug: String! status: ArticleSavingRequestStatus! - updatedAt: Date! + updatedAt: Date url: String! user: User! userId: ID! @deprecated(reason: "userId has been replaced with user") @@ -638,7 +638,7 @@ type Feature { id: ID! name: String! token: String! - updatedAt: Date! + updatedAt: Date } type FeedArticle { @@ -683,7 +683,7 @@ type Filter { id: ID! name: String! position: Int! - updatedAt: Date! + updatedAt: Date visible: Boolean } @@ -825,7 +825,7 @@ type Highlight { shortId: String! suffix: String type: HighlightType! - updatedAt: Date! + updatedAt: Date user: User! } @@ -834,7 +834,7 @@ type HighlightReply { highlight: Highlight! id: ID! text: String! - updatedAt: Date! + updatedAt: Date user: User! } @@ -871,7 +871,7 @@ type Integration { taskName: String token: String! type: IntegrationType! - updatedAt: Date! + updatedAt: Date } enum IntegrationType { @@ -964,7 +964,7 @@ type Link { shareInfo: LinkShareInfo! shareStats: ShareStats! slug: String! - updatedAt: Date! + updatedAt: Date url: String! } @@ -1217,7 +1217,7 @@ type Page { readableHtml: String! title: String! type: PageType! - updatedAt: Date! + updatedAt: Date url: String! } @@ -1444,7 +1444,7 @@ type RecommendationGroup { members: [User!]! name: String! topics: [String!] - updatedAt: Date! + updatedAt: Date } type RecommendingUser { @@ -1520,7 +1520,7 @@ type Rule { filter: String! id: ID! name: String! - updatedAt: Date! + updatedAt: Date } type RuleAction { @@ -2082,7 +2082,7 @@ type Subscription { type: SubscriptionType! unsubscribeHttpUrl: String unsubscribeMailTo: String - updatedAt: Date! + updatedAt: Date url: String } @@ -2585,7 +2585,7 @@ type Webhook { eventTypes: [WebhookEvent!]! id: ID! method: String! - updatedAt: Date! + updatedAt: Date url: String! } diff --git a/packages/api/src/repository/highlight.ts b/packages/api/src/repository/highlight.ts index ae66a19ce..232037267 100644 --- a/packages/api/src/repository/highlight.ts +++ b/packages/api/src/repository/highlight.ts @@ -26,10 +26,15 @@ export const highlightRepository = entityManager }) }, - createAndSave(highlight: DeepPartial, userId: string) { + createAndSave( + highlight: DeepPartial, + libraryItemId: string, + userId: string + ) { return this.save({ ...unescapeHighlight(highlight), user: { id: userId }, + libraryItem: { id: libraryItemId }, }) }, }) diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index 83ab3dfff..ca9b026b5 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -4,7 +4,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-floating-promises */ import { Readability } from '@omnivore/readability' -import graphqlFields from 'graphql-fields' import { LibraryItem, LibraryItemState, @@ -12,7 +11,6 @@ import { } from '../../entity/library_item' import { env } from '../../env' import { - Article, ArticleError, ArticleErrorCode, ArticleSavingRequestStatus, @@ -25,13 +23,11 @@ import { CreateArticleError, CreateArticleErrorCode, CreateArticleSuccess, - FeedArticle, MutationBulkActionArgs, MutationCreateArticleArgs, MutationSaveArticleReadingProgressArgs, MutationSetBookmarkArticleArgs, MutationSetFavoriteArticleArgs, - PageInfo, PageType, QueryArticleArgs, QuerySearchArgs, @@ -48,7 +44,6 @@ import { SetFavoriteArticleError, SetFavoriteArticleErrorCode, SetFavoriteArticleSuccess, - SetShareArticleSuccess, TypeaheadSearchError, TypeaheadSearchErrorCode, TypeaheadSearchSuccess, @@ -80,7 +75,6 @@ import { setFileUploadComplete, } from '../../services/upload_file' import { traceAs } from '../../tracing' -import { Merge } from '../../util' import { analytics } from '../../utils/analytics' import { isSiteBlockedForParse } from '../../utils/blocked' import { @@ -89,7 +83,7 @@ import { generateSlug, isBase64Image, isParsingTimeout, - libraryItemToPartialArticle, + libraryItemToArticle, libraryItemToSearchItem, pageError, titleForFilePath, @@ -117,16 +111,6 @@ export enum ArticleFormat { HighlightedMarkdown = 'highlightedMarkdown', } -export type PartialArticle = Omit< - Article, - | 'updatedAt' - | 'readingProgressPercent' - | 'readingProgressAnchorIndex' - | 'savedAt' - | 'highlights' - | 'contentReader' -> - // These two page types are better handled by the backend // where we can use APIs to fetch their underlying content. const FORCE_PUPPETEER_URLS = [ @@ -136,12 +120,8 @@ const FORCE_PUPPETEER_URLS = [ ] const UNPARSEABLE_CONTENT = '

We were unable to parse this page.

' -export type CreateArticlesSuccessPartial = Merge< - CreateArticleSuccess, - { createdArticle: PartialArticle } -> export const createArticleResolver = authorized< - CreateArticlesSuccessPartial, + CreateArticleSuccess, CreateArticleError, MutationCreateArticleArgs >( @@ -232,6 +212,11 @@ export const createArticleResolver = authorized< url, hash: '', isArchived: false, + readingProgressAnchorIndex: 0, + readingProgressPercent: 0, + highlights: [], + savedAt: new Date(), + updatedAt: new Date(), }, } @@ -378,7 +363,7 @@ export const createArticleResolver = authorized< return { user, created: true, - createdArticle: libraryItemToPartialArticle(libraryItemToReturn), + createdArticle: libraryItemToArticle(libraryItemToReturn), } } catch (error) { log.error('Error creating article', error) @@ -394,29 +379,28 @@ export const createArticleResolver = authorized< } ) -export type ArticleSuccessPartial = Merge< - ArticleSuccess, - { article: PartialArticle } -> export const getArticleResolver = authorized< - ArticleSuccessPartial, + ArticleSuccess, ArticleError, QueryArticleArgs >(async (_obj, { slug, format }, { authTrx, uid, log }, info) => { try { - const includeOriginalHtml = - format === ArticleFormat.Distiller || - !!graphqlFields(info).article.originalHtml + // const includeOriginalHtml = + // format === ArticleFormat.Distiller || + // !!graphqlFields(info).article.originalHtml // We allow the backend to use the ID instead of a slug to fetch the article const libraryItem = await authTrx((tx) => - tx - .withRepository(libraryItemRepository) - .createQueryBuilder('library_item') - .leftJoinAndSelect('library_item.labels', 'labels') - .leftJoinAndSelect('library_item.highlights', 'highlights') - .where('library_item.id = :id', { id: slug }) - .getOne() + tx.withRepository(libraryItemRepository).findOne({ + where: { slug }, + relations: { + labels: true, + highlights: { + user: true, + labels: true, + }, + }, + }) ) if (!libraryItem || libraryItem.state === LibraryItemState.Deleted) { @@ -444,7 +428,7 @@ export const getArticleResolver = authorized< } return { - article: libraryItemToPartialArticle(libraryItem), + article: libraryItemToArticle(libraryItem), } } catch (error) { log.error(error) @@ -452,26 +436,26 @@ export const getArticleResolver = authorized< } }) -type PaginatedPartialArticles = { - edges: { cursor: string; node: PartialArticle }[] - pageInfo: PageInfo -} +// type PaginatedPartialArticles = { +// edges: { cursor: string; node: PartialArticle }[] +// pageInfo: PageInfo +// } -export type SetShareArticleSuccessPartial = Merge< - SetShareArticleSuccess, - { - updatedFeedArticle?: Omit< - FeedArticle, - | 'sharedBy' - | 'article' - | 'highlightsCount' - | 'annotationsCount' - | 'reactions' - > - updatedFeedArticleId?: string - updatedArticle: PartialArticle - } -> +// export type SetShareArticleSuccessPartial = Merge< +// SetShareArticleSuccess, +// { +// updatedFeedArticle?: Omit< +// FeedArticle, +// | 'sharedBy' +// | 'article' +// | 'highlightsCount' +// | 'annotationsCount' +// | 'reactions' +// > +// updatedFeedArticleId?: string +// updatedArticle: PartialArticle +// } +// > // export const setShareArticleResolver = authorized< // SetShareArticleSuccessPartial, @@ -532,12 +516,8 @@ export type SetShareArticleSuccessPartial = Merge< // } // ) -export type SetBookmarkArticleSuccessPartial = Merge< - SetBookmarkArticleSuccess, - { bookmarkedArticle: PartialArticle } -> export const setBookmarkArticleResolver = authorized< - SetBookmarkArticleSuccessPartial, + SetBookmarkArticleSuccess, SetBookmarkArticleError, MutationSetBookmarkArticleArgs >(async (_, { input: { articleID } }, { uid, log, pubsub }) => { @@ -574,16 +554,12 @@ export const setBookmarkArticleResolver = authorized< }) // Make sure article.id instead of userArticle.id has passed. We use it for cache updates return { - bookmarkedArticle: libraryItemToPartialArticle(deletedLibraryItem), + bookmarkedArticle: libraryItemToArticle(deletedLibraryItem), } }) -export type SaveArticleReadingProgressSuccessPartial = Merge< - SaveArticleReadingProgressSuccess, - { updatedArticle: PartialArticle } -> export const saveArticleReadingProgressResolver = authorized< - SaveArticleReadingProgressSuccessPartial, + SaveArticleReadingProgressSuccess, SaveArticleReadingProgressError, MutationSaveArticleReadingProgressArgs >( @@ -648,7 +624,7 @@ export const saveArticleReadingProgressResolver = authorized< const updatedItem = await updateLibraryItem(id, updatedPart, uid, pubsub) return { - updatedArticle: libraryItemToPartialArticle(updatedItem), + updatedArticle: libraryItemToArticle(updatedItem), } } ) diff --git a/packages/api/src/resolvers/highlight/index.ts b/packages/api/src/resolvers/highlight/index.ts index ab956e8dd..7b58fe75c 100644 --- a/packages/api/src/resolvers/highlight/index.ts +++ b/packages/api/src/resolvers/highlight/index.ts @@ -29,9 +29,10 @@ import { } from '../../generated/graphql' import { highlightRepository } from '../../repository/highlight' import { + createHighlight, deleteHighlightById, mergeHighlights, - saveHighlight, + updateHighlight, } from '../../services/highlights' import { analytics } from '../../utils/analytics' import { authorized } from '../../utils/helpers' @@ -54,7 +55,12 @@ export const createHighlightResolver = authorized< MutationCreateHighlightArgs >(async (_, { input }, { log, pubsub, uid }) => { try { - const newHighlight = await saveHighlight(input, uid, pubsub) + const newHighlight = await createHighlight( + input, + input.articleId, + uid, + pubsub + ) analytics.track({ userId: uid, @@ -131,6 +137,7 @@ export const mergeHighlightResolver = authorized< const newHighlight = await mergeHighlights( overlapHighlightIdList, highlight, + input.articleId, uid, pubsub ) @@ -162,7 +169,12 @@ export const updateHighlightResolver = authorized< MutationUpdateHighlightArgs >(async (_, { input }, { pubsub, uid, log }) => { try { - const updatedHighlight = await saveHighlight(input, uid, pubsub) + const updatedHighlight = await updateHighlight( + input.highlightId, + input, + uid, + pubsub + ) return { highlight: highlightDataToHighlight(updatedHighlight) } } catch (error) { diff --git a/packages/api/src/resolvers/labels/index.ts b/packages/api/src/resolvers/labels/index.ts index ca825631c..edded1e98 100644 --- a/packages/api/src/resolvers/labels/index.ts +++ b/packages/api/src/resolvers/labels/index.ts @@ -276,6 +276,7 @@ export const setLabelsForHighlightResolver = authorized< } } } + // save labels in the library item await saveLabelsInHighlight(labelsSet, input.highlightId, uid, pubsub) @@ -293,7 +294,7 @@ export const setLabelsForHighlightResolver = authorized< labels: labelsSet, } } catch (error) { - log.error(error) + log.error('setLabelsForHighlightResolver error', error) return { errorCodes: [SetLabelsErrorCode.BadRequest], } diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 30034b201..14e257fd3 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -291,7 +291,7 @@ const schema = gql` slug: String! savedBy: User! savedAt: Date! - updatedAt: Date! + updatedAt: Date savedByViewer: Boolean! postedByViewer: Boolean! @@ -333,7 +333,7 @@ const schema = gql` originalHtml: String! readableHtml: String! createdAt: Date! - updatedAt: Date! + updatedAt: Date } type RecommendingUser { @@ -368,7 +368,7 @@ const schema = gql` originalHtml: String createdAt: Date! savedAt: Date! - updatedAt: Date! + updatedAt: Date publishedAt: Date readingProgressTopPercent: Float readingProgressPercent: Float! @@ -705,7 +705,7 @@ const schema = gql` replies: [HighlightReply!]! sharedAt: Date createdAt: Date! - updatedAt: Date! + updatedAt: Date reactions: [Reaction!]! createdByMe: Boolean! highlightPositionPercent: Float @@ -835,7 +835,7 @@ const schema = gql` highlight: Highlight! text: String! createdAt: Date! - updatedAt: Date! + updatedAt: Date } input CreateHighlightReplyInput { @@ -1109,7 +1109,7 @@ const schema = gql` status: ArticleSavingRequestStatus! errorCode: CreateArticleErrorCode createdAt: Date! - updatedAt: Date! + updatedAt: Date url: String! } @@ -1652,7 +1652,7 @@ const schema = gql` count: Int! lastFetchedAt: Date createdAt: Date! - updatedAt: Date! + updatedAt: Date } enum SubscriptionStatus { @@ -1757,7 +1757,7 @@ const schema = gql` method: String! enabled: Boolean! createdAt: Date! - updatedAt: Date! + updatedAt: Date } type SetWebhookError { @@ -1949,7 +1949,7 @@ const schema = gql` token: String! enabled: Boolean! createdAt: Date! - updatedAt: Date! + updatedAt: Date taskName: String } @@ -2047,7 +2047,7 @@ const schema = gql` name: String! token: String! createdAt: Date! - updatedAt: Date! + updatedAt: Date grantedAt: Date expiresAt: Date } @@ -2074,7 +2074,7 @@ const schema = gql` actions: [RuleAction!]! enabled: Boolean! createdAt: Date! - updatedAt: Date! + updatedAt: Date eventTypes: [RuleEventType!]! } @@ -2188,7 +2188,7 @@ const schema = gql` category: String! description: String createdAt: Date! - updatedAt: Date! + updatedAt: Date defaultFilter: Boolean visible: Boolean } @@ -2304,7 +2304,7 @@ const schema = gql` admins: [User!]! members: [User!]! createdAt: Date! - updatedAt: Date! + updatedAt: Date canPost: Boolean! description: String topics: [String!] diff --git a/packages/api/src/services/highlights.ts b/packages/api/src/services/highlights.ts index 77c1f2f82..e75003fc4 100644 --- a/packages/api/src/services/highlights.ts +++ b/packages/api/src/services/highlights.ts @@ -6,7 +6,7 @@ import { createPubSubClient, EntityType } from '../pubsub' import { authTrx, setClaims } from '../repository' import { highlightRepository } from '../repository/highlight' -type HighlightEvent = Highlight & { pageId: string } +type HighlightEvent = DeepPartial & { pageId: string } export const getHighlightLocation = (patch: string): number | undefined => { const dmp = new diff_match_patch() @@ -17,8 +17,9 @@ export const getHighlightLocation = (patch: string): number | undefined => { export const getHighlightUrl = (slug: string, highlightId: string): string => `${homePageURL()}/me/${slug}#${highlightId}` -export const saveHighlight = async ( +export const createHighlight = async ( highlight: DeepPartial, + libraryItemId: string, userId: string, pubsub = createPubSubClient() ) => { @@ -27,12 +28,12 @@ export const saveHighlight = async ( return tx .withRepository(highlightRepository) - .createAndSave(highlight, userId) + .createAndSave(highlight, libraryItemId, userId) }) await pubsub.entityCreated( EntityType.HIGHLIGHT, - { ...newHighlight, pageId: newHighlight.libraryItem.id }, + { ...newHighlight, pageId: libraryItemId }, userId ) @@ -42,6 +43,7 @@ export const saveHighlight = async ( export const mergeHighlights = async ( highlightsToRemove: string[], highlightToAdd: DeepPartial, + libraryItemId: string, userId: string, pubsub = createPubSubClient() ) => { @@ -50,18 +52,46 @@ export const mergeHighlights = async ( await highlightRepo.delete(highlightsToRemove) - return highlightRepo.createAndSave(highlightToAdd, userId) + return highlightRepo.createAndSave(highlightToAdd, libraryItemId, userId) }) await pubsub.entityCreated( EntityType.HIGHLIGHT, - { ...newHighlight, pageId: newHighlight.libraryItem.id }, + { ...newHighlight, pageId: libraryItemId }, userId ) return newHighlight } +export const updateHighlight = async ( + highlightId: string, + highlight: DeepPartial, + userId: string, + pubsub = createPubSubClient() +) => { + const updatedHighlight = await authTrx(async (tx) => { + await tx.withRepository(highlightRepository).save({ + ...highlight, + id: highlightId, + }) + + return tx.withRepository(highlightRepository).findById(highlightId) + }) + + if (!updatedHighlight) { + throw new Error(`Highlight ${highlightId} not found`) + } + + await pubsub.entityUpdated( + EntityType.HIGHLIGHT, + { ...highlight, id: highlightId, pageId: updatedHighlight.libraryItem.id }, + userId + ) + + return updatedHighlight +} + export const deleteHighlightById = async (highlightId: string) => { return authTrx(async (tx) => { const highlightRepo = tx.withRepository(highlightRepository) @@ -70,6 +100,8 @@ export const deleteHighlightById = async (highlightId: string) => { throw new Error(`Highlight ${highlightId} not found`) } - return highlightRepo.remove(highlight) + await highlightRepo.remove(highlight) + + return highlight }) } diff --git a/packages/api/src/services/labels.ts b/packages/api/src/services/labels.ts index d5db9c3ec..e8454c78b 100644 --- a/packages/api/src/services/labels.ts +++ b/packages/api/src/services/labels.ts @@ -57,7 +57,7 @@ export const saveLabelsInLibraryItem = async ( await authTrx(async (tx) => { await tx .withRepository(libraryItemRepository) - .update(libraryItemId, { labels }) + .save({ id: libraryItemId, labels }) }) // create pubsub event @@ -98,7 +98,9 @@ export const saveLabelsInHighlight = async ( pubsub = createPubSubClient() ) => { await authTrx(async (tx) => { - await tx.withRepository(highlightRepository).update(highlightId, { labels }) + await tx + .withRepository(highlightRepository) + .save({ id: highlightId, labels }) }) // create pubsub event diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index bc872d380..2a8025708 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -242,6 +242,8 @@ export const searchLibraryItems = async ( .createQueryBuilder(LibraryItem, 'library_item') .leftJoinAndSelect('library_item.labels', 'labels') .leftJoinAndSelect('library_item.highlights', 'highlights') + .leftJoinAndSelect('highlights.user', 'user') + .leftJoinAndSelect('user.profile', 'profile') .where('library_item.user_id = :userId', { userId }) // build the where clause diff --git a/packages/api/src/services/save_page.ts b/packages/api/src/services/save_page.ts index 73af6dd13..308c0fd5b 100644 --- a/packages/api/src/services/save_page.ts +++ b/packages/api/src/services/save_page.ts @@ -29,7 +29,7 @@ import { import { logger } from '../utils/logger' import { parsePreparedContent } from '../utils/parser' import { createPageSaveRequest } from './create_page_save_request' -import { saveHighlight } from './highlights' +import { createHighlight } from './highlights' import { findOrCreateLabels } from './labels' import { createLibraryItem, updateLibraryItem } from './library_item' @@ -174,7 +174,7 @@ export const savePage = async ( type: HighlightType.Highlight, } - if (!(await saveHighlight(highlight, user.id))) { + if (!(await createHighlight(highlight, clientRequestId, user.id))) { return { errorCodes: [SaveErrorCode.EmbeddedHighlightFailed], message: 'Failed to save highlight', diff --git a/packages/api/src/utils/helpers.ts b/packages/api/src/utils/helpers.ts index 78215e8aa..fd03bd992 100644 --- a/packages/api/src/utils/helpers.ts +++ b/packages/api/src/utils/helpers.ts @@ -10,10 +10,12 @@ import { LibraryItem, LibraryItemState } from '../entity/library_item' import { Recommendation as RecommendationData } from '../entity/recommendation' import { RegistrationType, User } from '../entity/user' import { + Article, ArticleSavingRequest, ArticleSavingRequestStatus, ContentReader, CreateArticleError, + CreateArticleSuccess, FeedArticle, Highlight, HighlightType, @@ -24,7 +26,6 @@ import { SearchItem, } from '../generated/graphql' import { createPubSubClient } from '../pubsub' -import { CreateArticlesSuccessPartial, PartialArticle } from '../resolvers' import { Claims, WithDataSourcesContext } from '../resolvers/types' import { validateUrl } from '../services/create_page_save_request' import { updateLibraryItem } from '../services/library_item' @@ -182,7 +183,7 @@ export const pageError = async ( userId: string, pageId?: string | null, pubsub = createPubSubClient() -): Promise => { +): Promise => { if (!pageId) return result await updateLibraryItem( @@ -225,9 +226,7 @@ export const libraryItemToArticleSavingRequest = ( userId: user.id, }) -export const libraryItemToPartialArticle = ( - item: LibraryItem -): PartialArticle => ({ +export const libraryItemToArticle = (item: LibraryItem): Article => ({ ...item, url: item.originalUrl, state: item.state as unknown as ArticleSavingRequestStatus, @@ -239,6 +238,10 @@ export const libraryItemToPartialArticle = ( ), subscription: item.subscription?.name, image: item.thumbnail, + contentReader: item.contentReader as unknown as ContentReader, + readingProgressAnchorIndex: item.readingProgressHighestReadAnchor, + readingProgressPercent: item.readingProgressTopPercent, + highlights: item.highlights?.map(highlightDataToHighlight) || [], }) export const libraryItemToSearchItem = (item: LibraryItem): SearchItem => ({ diff --git a/packages/db/migrations/0118.do.library_item.sql b/packages/db/migrations/0118.do.library_item.sql index 7a9cdef06..5a5fd4134 100755 --- a/packages/db/migrations/0118.do.library_item.sql +++ b/packages/db/migrations/0118.do.library_item.sql @@ -27,7 +27,7 @@ CREATE TABLE omnivore.library_item ( archived_at timestamptz, deleted_at timestamptz, read_at timestamptz, - updated_at timestamptz NOT NULL DEFAULT current_timestamp, + updated_at timestamptz, item_language text, word_count integer, site_name text, diff --git a/packages/db/migrations/0119.do.entity_labels.sql b/packages/db/migrations/0119.do.entity_labels.sql index 581851577..2bfa8a499 100755 --- a/packages/db/migrations/0119.do.entity_labels.sql +++ b/packages/db/migrations/0119.do.entity_labels.sql @@ -8,7 +8,9 @@ CREATE TABLE omnivore.entity_labels ( id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(), library_item_id uuid REFERENCES omnivore.library_item(id) ON DELETE CASCADE, highlight_id uuid REFERENCES omnivore.highlight(id) ON DELETE CASCADE, - label_id uuid NOT NULL REFERENCES omnivore.labels(id) ON DELETE CASCADE + label_id uuid NOT NULL REFERENCES omnivore.labels(id) ON DELETE CASCADE, + unique(library_item_id, label_id), + unique(highlight_id, label_id) ); GRANT SELECT, INSERT, DELETE ON omnivore.entity_labels TO omnivore_user; @@ -36,7 +38,7 @@ BEGIN ) -- Update label_names on library_item UPDATE omnivore.library_item li - SET label_names = l.names_agg + SET label_names = coalesce(l.names_agg, array[]::text[]) FROM labels_agg l WHERE li.id = current_library_item_id; ELSIF current_highlight_id IS NOT NULL THEN @@ -51,7 +53,7 @@ BEGIN ) -- Update highlight_labels on library_item UPDATE omnivore.library_item li - SET highlight_labels = l.names_agg + SET highlight_labels = coalesce(l.names_agg, array[]::text[]) FROM labels_agg l WHERE li.id = current_library_item_id; END IF; diff --git a/packages/db/migrations/0120.do.alter_labels_table.sql b/packages/db/migrations/0120.do.alter_labels_table.sql new file mode 100755 index 000000000..e5269c289 --- /dev/null +++ b/packages/db/migrations/0120.do.alter_labels_table.sql @@ -0,0 +1,17 @@ +-- Type: DO +-- Name: alter_labels_table +-- Description: Alter labels table + +BEGIN; + +ALTER TABLE omnivore.labels ADD COLUMN updated_at timestamptz; + +CREATE TRIGGER update_labels_modtime BEFORE UPDATE ON omnivore.labels + FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); + +ALTER TABLE omnivore.abuse_report DROP COLUMN page_id; +ALTER TABLE omnivore.abuse_report RENAME COLUMN elastic_page_id TO library_item_id; +ALTER TABLE omnivore.content_display_report DROP COLUMN page_id; +ALTER TABLE omnivore.content_display_report RENAME COLUMN elastic_page_id TO library_item_id; + +COMMIT; diff --git a/packages/db/migrations/0120.do.library_item_preview.sql b/packages/db/migrations/0120.do.library_item_preview.sql deleted file mode 100755 index c8c3cdda9..000000000 --- a/packages/db/migrations/0120.do.library_item_preview.sql +++ /dev/null @@ -1,23 +0,0 @@ --- Type: DO --- Name: library_item_preview --- Description: Create library_item_preview table - -BEGIN; - -CREATE TABLE omnivore.library_item_preview ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v1mc(), - sender_id uuid NOT NULL REFERENCES omnivore.user ON DELETE CASCADE, - recipient_ids uuid[] NOT NULL, -- Array of user ids - library_item_id uuid NOT NULL REFERENCES omnivore.library_item(id) ON DELETE CASCADE, - thumbnail text, - includes_note bool NOT NULL DEFAULT false, - includes_highlight bool NOT NULL DEFAULT false, - created_at timestamptz NOT NULL DEFAULT current_timestamp, - updated_at timestamptz NOT NULL DEFAULT current_timestamp -); - -GRANT SELECT, INSERT ON omnivore.library_item_preview TO omnivore_user; - -CREATE TRIGGER update_library_item_preview_modtime BEFORE UPDATE ON omnivore.library_item_preview FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); - -COMMIT; diff --git a/packages/db/migrations/0120.undo.alter_labels_table.sql b/packages/db/migrations/0120.undo.alter_labels_table.sql new file mode 100755 index 000000000..28b409352 --- /dev/null +++ b/packages/db/migrations/0120.undo.alter_labels_table.sql @@ -0,0 +1,16 @@ +-- Type: UNDO +-- Name: alter_labels_table +-- Description: Alter labels table + +BEGIN; + +ALTER TABLE omnivore.abuse_report RENAME COLUMN library_item_id TO elastic_page_id; +ALTER TABLE omnivore.abuse_report ADD COLUMN page_id text; +ALTER TABLE omnivore.content_display_report RENAME COLUMN library_item_id TO elastic_page_id; +ALTER TABLE omnivore.content_display_report ADD COLUMN page_id text; + +DROP TRIGGER update_labels_modtime ON omnivore.labels; + +ALTER TABLE omnivore.labels DROP COLUMN updated_at; + +COMMIT; diff --git a/packages/db/migrations/0120.undo.library_item_preview.sql b/packages/db/migrations/0120.undo.library_item_preview.sql deleted file mode 100755 index 0644e036d..000000000 --- a/packages/db/migrations/0120.undo.library_item_preview.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Type: UNDO --- Name: library_item_preview --- Description: Create library_item_preview table - -BEGIN; - -DROP TRIGGER update_library_item_preview_modtime ON omnivore.library_item_preview; - -DROP TABLE omnivore.library_item_preview; - -COMMIT; diff --git a/packages/db/migrations/0121.do.update_highlight.sql b/packages/db/migrations/0121.do.update_highlight.sql index f230273dc..2e2a9829a 100755 --- a/packages/db/migrations/0121.do.update_highlight.sql +++ b/packages/db/migrations/0121.do.update_highlight.sql @@ -51,7 +51,7 @@ BEGIN WHERE library_item_id = current_library_item_id ) UPDATE omnivore.library_item li - SET highlight_annotations = h.annotation_agg + SET highlight_annotations = coalesce(h.annotation_agg, array[]::text[]) FROM highlight_agg h WHERE li.id = current_library_item_id; diff --git a/packages/db/migrations/0123.do.add_rls.sql b/packages/db/migrations/0123.do.add_rls.sql index baa036a46..5603cbe6e 100755 --- a/packages/db/migrations/0123.do.add_rls.sql +++ b/packages/db/migrations/0123.do.add_rls.sql @@ -61,9 +61,6 @@ CREATE POLICY search_history_policy on omnivore.search_history WITH CHECK (user_id = omnivore.get_current_user_id()); GRANT SELECT, INSERT, DELETE ON omnivore.search_history TO omnivore_user; -ALTER TABLE omnivore.abuse_report DROP COLUMN page_id; -ALTER TABLE omnivore.abuse_report RENAME COLUMN elastic_page_id TO library_item_id; -ALTER TABLE omnivore.content_display_report DROP COLUMN page_id; -ALTER TABLE omnivore.content_display_report RENAME COLUMN elastic_page_id TO library_item_id; + COMMIT; diff --git a/packages/db/migrations/0123.undo.add_rls.sql b/packages/db/migrations/0123.undo.add_rls.sql index 741a21967..876b00e2d 100755 --- a/packages/db/migrations/0123.undo.add_rls.sql +++ b/packages/db/migrations/0123.undo.add_rls.sql @@ -32,9 +32,4 @@ DROP POLICY user_device_tokens_policy on omnivore.user_device_tokens; ALTER TABLE omnivore.search_history DISABLE ROW LEVEL SECURITY; DROP POLICY search_history_policy on omnivore.search_history; -ALTER TABLE omnivore.abuse_report RENAME COLUMN library_item_id TO elastic_page_id; -ALTER TABLE omnivore.abuse_report ADD COLUMN page_id text; -ALTER TABLE omnivore.content_display_report RENAME COLUMN library_item_id TO elastic_page_id; -ALTER TABLE omnivore.content_display_report ADD COLUMN page_id text; - COMMIT; diff --git a/yarn.lock b/yarn.lock index 7edce9dfe..321becb4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10725,9 +10725,9 @@ camelcase@^6.0.0, camelcase@^6.2.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001251, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001317, caniuse-lite@^1.0.30001332: - version "1.0.30001462" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001462.tgz" - integrity sha512-PDd20WuOBPiasZ7KbFnmQRyuLE7cFXW2PVd7dmALzbkUXEP46upAuCDm9eY9vho8fgNMGmbAX92QBZHzcnWIqw== + version "1.0.30001527" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001527.tgz" + integrity sha512-YkJi7RwPgWtXVSgK4lG9AHH57nSzvvOp9MesgXmw4Q7n0C3H04L0foHqfxcmSAm5AcWb8dW9AYj2tR7/5GnddQ== capital-case@^1.0.4: version "1.0.4"