diff --git a/packages/api/src/entity/library_item.ts b/packages/api/src/entity/library_item.ts index 07cc2b879..e2cf946d6 100644 --- a/packages/api/src/entity/library_item.ts +++ b/packages/api/src/entity/library_item.ts @@ -207,7 +207,7 @@ export class LibraryItem { sharedBy?: string | null @Column('jsonb') - links?: Record[] | null + links?: any | null @Column('text') previewContent?: string | null @@ -217,4 +217,7 @@ export class LibraryItem { @Column('boolean') isInLibrary!: boolean + + @Column('text') + sharedSource?: string | null } diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index d3f3147c8..86039cb1a 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -837,6 +837,26 @@ export type FiltersSuccess = { filters: Array; }; +export type Following = { + __typename?: 'Following'; + SharedAt: Scalars['Date']; + author?: Maybe; + createdAt: Scalars['Date']; + description?: Maybe; + hiddenAt?: Maybe; + id: Scalars['ID']; + image?: Maybe; + links?: Maybe; + previewContent?: Maybe; + publishedAt?: Maybe; + seenAt?: Maybe; + sharedBy: Scalars['String']; + sharedSource: Scalars['String']; + title: Scalars['String']; + updatedAt: Scalars['Date']; + url: Scalars['String']; +}; + export type GenerateApiKeyError = { __typename?: 'GenerateApiKeyError'; errorCodes: Array; @@ -1320,6 +1340,7 @@ export type Mutation = { saveArticleReadingProgress: SaveArticleReadingProgressResult; saveFile: SaveResult; saveFilter: SaveFilterResult; + saveFollowing: SaveFollowingResult; savePage: SaveResult; saveUrl: SaveResult; setBookmarkArticle: SetBookmarkArticleResult; @@ -1516,6 +1537,11 @@ export type MutationSaveFilterArgs = { }; +export type MutationSaveFollowingArgs = { + input: SaveFollowingInput; +}; + + export type MutationSavePageArgs = { input: SavePageInput; }; @@ -2211,6 +2237,36 @@ export type SaveFilterSuccess = { filter: Filter; }; +export type SaveFollowingError = { + __typename?: 'SaveFollowingError'; + errorCodes: Array; +}; + +export enum SaveFollowingErrorCode { + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' +} + +export type SaveFollowingInput = { + author?: InputMaybe; + description?: InputMaybe; + links?: InputMaybe; + previewContent?: InputMaybe; + publishedAt?: InputMaybe; + sharedAt: Scalars['Date']; + sharedBy: Scalars['String']; + sharedSource: Scalars['String']; + title: Scalars['String']; + url: Scalars['String']; +}; + +export type SaveFollowingResult = SaveFollowingError | SaveFollowingSuccess; + +export type SaveFollowingSuccess = { + __typename?: 'SaveFollowingSuccess'; + following: Following; +}; + export type SavePageInput = { clientRequestId: Scalars['ID']; labels?: InputMaybe>; @@ -2257,10 +2313,8 @@ export enum SearchErrorCode { export type SearchItem = { __typename?: 'SearchItem'; - annotation?: Maybe; archivedAt?: Maybe; author?: Maybe; - color?: Maybe; content?: Maybe; contentReader: ContentReader; createdAt: Scalars['Date']; @@ -2273,25 +2327,20 @@ export type SearchItem = { language?: Maybe; originalArticleUrl?: Maybe; ownedByViewer?: Maybe; - pageId?: Maybe; pageType: PageType; publishedAt?: Maybe; - quote?: Maybe; readAt?: Maybe; readingProgressAnchorIndex: Scalars['Int']; readingProgressPercent: Scalars['Float']; readingProgressTopPercent?: Maybe; recommendations?: Maybe>; savedAt: Scalars['Date']; - shortId?: Maybe; siteIcon?: Maybe; siteName?: Maybe; slug: Scalars['String']; state?: Maybe; subscription?: Maybe; title: Scalars['String']; - unsubHttpUrl?: Maybe; - unsubMailTo?: Maybe; updatedAt?: Maybe; uploadFileId?: Maybe; url: Scalars['String']; @@ -3532,6 +3581,7 @@ export type ResolversTypes = { FiltersResult: ResolversTypes['FiltersError'] | ResolversTypes['FiltersSuccess']; FiltersSuccess: ResolverTypeWrapper; Float: ResolverTypeWrapper; + Following: ResolverTypeWrapper; GenerateApiKeyError: ResolverTypeWrapper; GenerateApiKeyErrorCode: GenerateApiKeyErrorCode; GenerateApiKeyInput: GenerateApiKeyInput; @@ -3697,6 +3747,11 @@ export type ResolversTypes = { SaveFilterInput: SaveFilterInput; SaveFilterResult: ResolversTypes['SaveFilterError'] | ResolversTypes['SaveFilterSuccess']; SaveFilterSuccess: ResolverTypeWrapper; + SaveFollowingError: ResolverTypeWrapper; + SaveFollowingErrorCode: SaveFollowingErrorCode; + SaveFollowingInput: SaveFollowingInput; + SaveFollowingResult: ResolversTypes['SaveFollowingError'] | ResolversTypes['SaveFollowingSuccess']; + SaveFollowingSuccess: ResolverTypeWrapper; SavePageInput: SavePageInput; SaveResult: ResolversTypes['SaveError'] | ResolversTypes['SaveSuccess']; SaveSuccess: ResolverTypeWrapper; @@ -4016,6 +4071,7 @@ export type ResolversParentTypes = { FiltersResult: ResolversParentTypes['FiltersError'] | ResolversParentTypes['FiltersSuccess']; FiltersSuccess: FiltersSuccess; Float: Scalars['Float']; + Following: Following; GenerateApiKeyError: GenerateApiKeyError; GenerateApiKeyInput: GenerateApiKeyInput; GenerateApiKeyResult: ResolversParentTypes['GenerateApiKeyError'] | ResolversParentTypes['GenerateApiKeySuccess']; @@ -4145,6 +4201,10 @@ export type ResolversParentTypes = { SaveFilterInput: SaveFilterInput; SaveFilterResult: ResolversParentTypes['SaveFilterError'] | ResolversParentTypes['SaveFilterSuccess']; SaveFilterSuccess: SaveFilterSuccess; + SaveFollowingError: SaveFollowingError; + SaveFollowingInput: SaveFollowingInput; + SaveFollowingResult: ResolversParentTypes['SaveFollowingError'] | ResolversParentTypes['SaveFollowingSuccess']; + SaveFollowingSuccess: SaveFollowingSuccess; SavePageInput: SavePageInput; SaveResult: ResolversParentTypes['SaveError'] | ResolversParentTypes['SaveSuccess']; SaveSuccess: SaveSuccess; @@ -4898,6 +4958,26 @@ export type FiltersSuccessResolvers; }; +export type FollowingResolvers = { + SharedAt?: Resolver; + author?: Resolver, ParentType, ContextType>; + createdAt?: Resolver; + description?: Resolver, ParentType, ContextType>; + hiddenAt?: Resolver, ParentType, ContextType>; + id?: Resolver; + image?: Resolver, ParentType, ContextType>; + links?: Resolver, ParentType, ContextType>; + previewContent?: Resolver, ParentType, ContextType>; + publishedAt?: Resolver, ParentType, ContextType>; + seenAt?: Resolver, ParentType, ContextType>; + sharedBy?: Resolver; + sharedSource?: Resolver; + title?: Resolver; + updatedAt?: Resolver; + url?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type GenerateApiKeyErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -5263,6 +5343,7 @@ export type MutationResolvers>; saveFile?: Resolver>; saveFilter?: Resolver>; + saveFollowing?: Resolver>; savePage?: Resolver>; saveUrl?: Resolver>; setBookmarkArticle?: Resolver>; @@ -5622,6 +5703,20 @@ export type SaveFilterSuccessResolvers; }; +export type SaveFollowingErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type SaveFollowingResultResolvers = { + __resolveType: TypeResolveFn<'SaveFollowingError' | 'SaveFollowingSuccess', ParentType, ContextType>; +}; + +export type SaveFollowingSuccessResolvers = { + following?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type SaveResultResolvers = { __resolveType: TypeResolveFn<'SaveError' | 'SaveSuccess', ParentType, ContextType>; }; @@ -5638,10 +5733,8 @@ export type SearchErrorResolvers = { - annotation?: Resolver, ParentType, ContextType>; archivedAt?: Resolver, ParentType, ContextType>; author?: Resolver, ParentType, ContextType>; - color?: Resolver, ParentType, ContextType>; content?: Resolver, ParentType, ContextType>; contentReader?: Resolver; createdAt?: Resolver; @@ -5654,25 +5747,20 @@ export type SearchItemResolvers, ParentType, ContextType>; originalArticleUrl?: Resolver, ParentType, ContextType>; ownedByViewer?: Resolver, ParentType, ContextType>; - pageId?: Resolver, ParentType, ContextType>; pageType?: Resolver; publishedAt?: Resolver, ParentType, ContextType>; - quote?: Resolver, ParentType, ContextType>; readAt?: Resolver, ParentType, ContextType>; readingProgressAnchorIndex?: Resolver; readingProgressPercent?: Resolver; readingProgressTopPercent?: Resolver, ParentType, ContextType>; recommendations?: Resolver>, ParentType, ContextType>; savedAt?: Resolver; - shortId?: Resolver, ParentType, ContextType>; siteIcon?: Resolver, ParentType, ContextType>; siteName?: Resolver, ParentType, ContextType>; slug?: Resolver; state?: Resolver, ParentType, ContextType>; subscription?: Resolver, ParentType, ContextType>; title?: Resolver; - unsubHttpUrl?: Resolver, ParentType, ContextType>; - unsubMailTo?: Resolver, ParentType, ContextType>; updatedAt?: Resolver, ParentType, ContextType>; uploadFileId?: Resolver, ParentType, ContextType>; url?: Resolver; @@ -6403,6 +6491,7 @@ export type Resolvers = { FiltersError?: FiltersErrorResolvers; FiltersResult?: FiltersResultResolvers; FiltersSuccess?: FiltersSuccessResolvers; + Following?: FollowingResolvers; GenerateApiKeyError?: GenerateApiKeyErrorResolvers; GenerateApiKeyResult?: GenerateApiKeyResultResolvers; GenerateApiKeySuccess?: GenerateApiKeySuccessResolvers; @@ -6513,6 +6602,9 @@ export type Resolvers = { SaveFilterError?: SaveFilterErrorResolvers; SaveFilterResult?: SaveFilterResultResolvers; SaveFilterSuccess?: SaveFilterSuccessResolvers; + SaveFollowingError?: SaveFollowingErrorResolvers; + SaveFollowingResult?: SaveFollowingResultResolvers; + SaveFollowingSuccess?: SaveFollowingSuccessResolvers; SaveResult?: SaveResultResolvers; SaveSuccess?: SaveSuccessResolvers; SearchError?: SearchErrorResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 5e1ae27af..120c9d275 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -742,6 +742,25 @@ type FiltersSuccess { filters: [Filter!]! } +type Following { + SharedAt: Date! + author: String + createdAt: Date! + description: String + hiddenAt: Date + id: ID! + image: String + links: JSON + previewContent: String + publishedAt: Date + seenAt: Date + sharedBy: String! + sharedSource: String! + title: String! + updatedAt: Date! + url: String! +} + type GenerateApiKeyError { errorCodes: [GenerateApiKeyErrorCode!]! } @@ -1185,6 +1204,7 @@ type Mutation { saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult! saveFile(input: SaveFileInput!): SaveResult! saveFilter(input: SaveFilterInput!): SaveFilterResult! + saveFollowing(input: SaveFollowingInput!): SaveFollowingResult! savePage(input: SavePageInput!): SaveResult! saveUrl(input: SaveUrlInput!): SaveResult! setBookmarkArticle(input: SetBookmarkArticleInput!): SetBookmarkArticleResult! @@ -1678,6 +1698,34 @@ type SaveFilterSuccess { filter: Filter! } +type SaveFollowingError { + errorCodes: [SaveFollowingErrorCode!]! +} + +enum SaveFollowingErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +input SaveFollowingInput { + author: String + description: String + links: JSON + previewContent: String + publishedAt: Date + sharedAt: Date! + sharedBy: String! + sharedSource: String! + title: String! + url: String! +} + +union SaveFollowingResult = SaveFollowingError | SaveFollowingSuccess + +type SaveFollowingSuccess { + following: Following! +} + input SavePageInput { clientRequestId: ID! labels: [CreateLabelInput!] @@ -1721,10 +1769,8 @@ enum SearchErrorCode { } type SearchItem { - annotation: String archivedAt: Date author: String - color: String content: String contentReader: ContentReader! createdAt: Date! @@ -1737,25 +1783,20 @@ type SearchItem { language: String originalArticleUrl: String ownedByViewer: Boolean - pageId: ID pageType: PageType! publishedAt: Date - quote: String readAt: Date readingProgressAnchorIndex: Int! readingProgressPercent: Float! readingProgressTopPercent: Float recommendations: [Recommendation!] savedAt: Date! - shortId: String siteIcon: String siteName: String slug: String! state: ArticleSavingRequestStatus subscription: String title: String! - unsubHttpUrl: String - unsubMailTo: String updatedAt: Date uploadFileId: ID url: String! diff --git a/packages/api/src/resolvers/following/index.ts b/packages/api/src/resolvers/following/index.ts index 08c219436..b97951e54 100644 --- a/packages/api/src/resolvers/following/index.ts +++ b/packages/api/src/resolvers/following/index.ts @@ -3,9 +3,13 @@ import { FeedsError, FeedsErrorCode, FeedsSuccess, + MutationSaveFollowingArgs, QueryFeedsArgs, + SaveFollowingError, + SaveFollowingSuccess, } from '../../generated/graphql' import { feedRepository } from '../../repository/feed' +import { createFollowing } from '../../services/library_item' import { authorized } from '../../utils/helpers' export const feedsResolve = authorized< @@ -59,3 +63,22 @@ export const feedsResolve = authorized< } } }) + +export const saveFollowingResolver = authorized< + SaveFollowingSuccess, + SaveFollowingError, + MutationSaveFollowingArgs +>(async (_, { input }, { uid }) => { + const newItem = await createFollowing(input, uid) + + return { + __typename: 'SaveFollowingSuccess', + following: { + ...newItem, + url: newItem.originalUrl, + SharedAt: new Date(input.sharedAt), + sharedBy: input.sharedBy, + sharedSource: input.sharedSource, + }, + } +}) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index e272c9d76..f5420fc29 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -1568,7 +1568,6 @@ const schema = gql` union SearchResult = SearchSuccess | SearchError type SearchItem { - # used for pages id: ID! title: String! slug: String! @@ -1590,16 +1589,8 @@ const schema = gql` # for uploaded file articles (PDFs), we track the original article URL separately! originalArticleUrl: String uploadFileId: ID - # used for highlights - pageId: ID - shortId: String - quote: String - annotation: String - color: String labels: [Label!] subscription: String - unsubMailTo: String - unsubHttpUrl: String state: ArticleSavingRequestStatus siteName: String language: String @@ -2649,6 +2640,53 @@ const schema = gql` author: String } + union SaveFollowingResult = SaveFollowingSuccess | SaveFollowingError + + type SaveFollowingSuccess { + following: Following! + } + + type Following { + id: ID! + title: String! + url: String! + author: String + image: String + description: String + seenAt: Date + createdAt: Date! + updatedAt: Date! + publishedAt: Date + hiddenAt: Date + SharedAt: Date! + sharedBy: String! + links: JSON + previewContent: String + sharedSource: String! + } + + type SaveFollowingError { + errorCodes: [SaveFollowingErrorCode!]! + } + + enum SaveFollowingErrorCode { + UNAUTHORIZED + BAD_REQUEST + } + + input SaveFollowingInput { + url: String! + title: String! + author: String + description: String + publishedAt: Date + sharedSource: String! + links: JSON + previewContent: String + sharedBy: String! + sharedAt: Date! + } + # Mutations type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! @@ -2752,6 +2790,7 @@ const schema = gql` updateSubscription( input: UpdateSubscriptionInput! ): UpdateSubscriptionResult! + saveFollowing(input: SaveFollowingInput!): SaveFollowingResult! } # FIXME: remove sort from feedArticles after all cached tabs are closed diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index fd79746ef..8f462849c 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -4,7 +4,7 @@ import { EntityLabel } from '../entity/entity_label' import { Highlight } from '../entity/highlight' import { Label } from '../entity/label' import { LibraryItem, LibraryItemState } from '../entity/library_item' -import { BulkActionType } from '../generated/graphql' +import { BulkActionType, SaveFollowingInput } from '../generated/graphql' import { createPubSubClient, EntityType } from '../pubsub' import { authTrx, getColumns } from '../repository' import { libraryItemRepository } from '../repository/library_item' @@ -567,6 +567,28 @@ export const createLibraryItem = async ( return newLibraryItem } +export const createFollowing = async ( + input: SaveFollowingInput, + userId: string +): Promise => { + return createLibraryItem( + { + ...input, + originalUrl: input.url, + isInLibrary: false, + state: LibraryItemState.Succeeded, + wordCount: 0, + user: { id: userId }, + sharedAt: new Date(input.sharedAt), + sharedSource: input.sharedSource, + sharedBy: input.sharedBy, + }, + userId, + undefined, + true + ) +} + export const findLibraryItemsByPrefix = async ( prefix: string, userId: string, diff --git a/packages/db/migrations/0146.do.following.sql b/packages/db/migrations/0146.do.following.sql index ba024fe7c..318aa3b59 100755 --- a/packages/db/migrations/0146.do.following.sql +++ b/packages/db/migrations/0146.do.following.sql @@ -15,6 +15,7 @@ ALTER TABLE omnivore.library_item ADD COLUMN links jsonb, ADD COLUMN preview_content text, ADD COLUMN seen_at timestamptz, + ADD COLUMN shared_source text, ADD COLUMN is_in_library boolean NOT NULL DEFAULT true; CREATE TABLE omnivore.feed ( diff --git a/packages/db/migrations/0146.undo.following.sql b/packages/db/migrations/0146.undo.following.sql index 2c323ec4b..e134e80a2 100755 --- a/packages/db/migrations/0146.undo.following.sql +++ b/packages/db/migrations/0146.undo.following.sql @@ -13,6 +13,7 @@ ALTER TABLE omnivore.library_item DROP COLUMN links, DROP COLUMN preview_content, DROP COLUMN seen_at, + DROP COLUMN shared_source, DROP COLUMN is_in_library; ALTER TABLE omnivore.subscriptions