mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #4075 from omnivore-app/feature/posts
feat: posts api
This commit is contained in:
commit
ee5e66d9b3
12 changed files with 1533 additions and 31 deletions
47
packages/api/src/entity/post.ts
Normal file
47
packages/api/src/entity/post.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm'
|
||||
import { User } from './user'
|
||||
|
||||
@Entity({ name: 'post' })
|
||||
export class Post {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string
|
||||
|
||||
@Column('uuid')
|
||||
userId!: string
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User
|
||||
|
||||
@Column('uuid', { array: true })
|
||||
libraryItemIds!: string[]
|
||||
|
||||
@Column('uuid', { array: true, nullable: true })
|
||||
highlightIds?: string[] | null
|
||||
|
||||
@Column('text')
|
||||
title!: string
|
||||
|
||||
@Column('text')
|
||||
content!: string
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
thumbnail?: string | null
|
||||
|
||||
@Column('text', { nullable: true })
|
||||
thought?: string | null
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date
|
||||
}
|
||||
|
|
@ -509,6 +509,32 @@ export type CreateNewsletterEmailSuccess = {
|
|||
newsletterEmail: NewsletterEmail;
|
||||
};
|
||||
|
||||
export type CreatePostError = {
|
||||
__typename?: 'CreatePostError';
|
||||
errorCodes: Array<CreatePostErrorCode>;
|
||||
};
|
||||
|
||||
export enum CreatePostErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type CreatePostInput = {
|
||||
content: Scalars['String'];
|
||||
highlightIds?: InputMaybe<Array<Scalars['ID']>>;
|
||||
libraryItemIds: Array<Scalars['ID']>;
|
||||
thought?: InputMaybe<Scalars['String']>;
|
||||
thumbnail?: InputMaybe<Scalars['String']>;
|
||||
title: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreatePostResult = CreatePostError | CreatePostSuccess;
|
||||
|
||||
export type CreatePostSuccess = {
|
||||
__typename?: 'CreatePostSuccess';
|
||||
post: Post;
|
||||
};
|
||||
|
||||
export type CreateReactionError = {
|
||||
__typename?: 'CreateReactionError';
|
||||
errorCodes: Array<CreateReactionErrorCode>;
|
||||
|
|
@ -749,6 +775,23 @@ export type DeleteNewsletterEmailSuccess = {
|
|||
newsletterEmail: NewsletterEmail;
|
||||
};
|
||||
|
||||
export type DeletePostError = {
|
||||
__typename?: 'DeletePostError';
|
||||
errorCodes: Array<DeletePostErrorCode>;
|
||||
};
|
||||
|
||||
export enum DeletePostErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type DeletePostResult = DeletePostError | DeletePostSuccess;
|
||||
|
||||
export type DeletePostSuccess = {
|
||||
__typename?: 'DeletePostSuccess';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type DeleteReactionError = {
|
||||
__typename?: 'DeleteReactionError';
|
||||
errorCodes: Array<DeleteReactionErrorCode>;
|
||||
|
|
@ -1800,6 +1843,7 @@ export type Mutation = {
|
|||
createHighlight: CreateHighlightResult;
|
||||
createLabel: CreateLabelResult;
|
||||
createNewsletterEmail: CreateNewsletterEmailResult;
|
||||
createPost: CreatePostResult;
|
||||
deleteAccount: DeleteAccountResult;
|
||||
deleteDiscoverArticle: DeleteDiscoverArticleResult;
|
||||
deleteDiscoverFeed: DeleteDiscoverFeedResult;
|
||||
|
|
@ -1809,6 +1853,7 @@ export type Mutation = {
|
|||
deleteIntegration: DeleteIntegrationResult;
|
||||
deleteLabel: DeleteLabelResult;
|
||||
deleteNewsletterEmail: DeleteNewsletterEmailResult;
|
||||
deletePost: DeletePostResult;
|
||||
deleteRule: DeleteRuleResult;
|
||||
deleteWebhook: DeleteWebhookResult;
|
||||
editDiscoverFeed: EditDiscoverFeedResult;
|
||||
|
|
@ -1859,6 +1904,7 @@ export type Mutation = {
|
|||
updateLabel: UpdateLabelResult;
|
||||
updateNewsletterEmail: UpdateNewsletterEmailResult;
|
||||
updatePage: UpdatePageResult;
|
||||
updatePost: UpdatePostResult;
|
||||
updateSubscription: UpdateSubscriptionResult;
|
||||
updateUser: UpdateUserResult;
|
||||
updateUserProfile: UpdateUserProfileResult;
|
||||
|
|
@ -1922,6 +1968,11 @@ export type MutationCreateNewsletterEmailArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationCreatePostArgs = {
|
||||
input: CreatePostInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteAccountArgs = {
|
||||
userID: Scalars['ID'];
|
||||
};
|
||||
|
|
@ -1967,6 +2018,11 @@ export type MutationDeleteNewsletterEmailArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationDeletePostArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteRuleArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
|
@ -2205,6 +2261,11 @@ export type MutationUpdatePageArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationUpdatePostArgs = {
|
||||
input: UpdatePostInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateSubscriptionArgs = {
|
||||
input: UpdateSubscriptionInput;
|
||||
};
|
||||
|
|
@ -2346,6 +2407,63 @@ export type ParseResult = {
|
|||
title: Scalars['String'];
|
||||
};
|
||||
|
||||
export type Post = {
|
||||
__typename?: 'Post';
|
||||
author: Scalars['String'];
|
||||
content: Scalars['String'];
|
||||
createdAt: Scalars['Date'];
|
||||
highlights?: Maybe<Array<Highlight>>;
|
||||
id: Scalars['ID'];
|
||||
libraryItems?: Maybe<Array<Article>>;
|
||||
ownedByViewer: Scalars['Boolean'];
|
||||
thought?: Maybe<Scalars['String']>;
|
||||
thumbnail?: Maybe<Scalars['String']>;
|
||||
title: Scalars['String'];
|
||||
updatedAt: Scalars['Date'];
|
||||
};
|
||||
|
||||
export type PostEdge = {
|
||||
__typename?: 'PostEdge';
|
||||
cursor: Scalars['String'];
|
||||
node: Post;
|
||||
};
|
||||
|
||||
export type PostError = {
|
||||
__typename?: 'PostError';
|
||||
errorCodes: Array<PostErrorCode>;
|
||||
};
|
||||
|
||||
export enum PostErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
NotFound = 'NOT_FOUND',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type PostResult = PostError | PostSuccess;
|
||||
|
||||
export type PostSuccess = {
|
||||
__typename?: 'PostSuccess';
|
||||
post: Post;
|
||||
};
|
||||
|
||||
export type PostsError = {
|
||||
__typename?: 'PostsError';
|
||||
errorCodes: Array<PostsErrorCode>;
|
||||
};
|
||||
|
||||
export enum PostsErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type PostsResult = PostsError | PostsSuccess;
|
||||
|
||||
export type PostsSuccess = {
|
||||
__typename?: 'PostsSuccess';
|
||||
edges: Array<PostEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type PreparedDocumentInput = {
|
||||
document: Scalars['String'];
|
||||
pageInfo: PageInfoInput;
|
||||
|
|
@ -2383,6 +2501,8 @@ export type Query = {
|
|||
labels: LabelsResult;
|
||||
me?: Maybe<User>;
|
||||
newsletterEmails: NewsletterEmailsResult;
|
||||
post: PostResult;
|
||||
posts: PostsResult;
|
||||
recentEmails: RecentEmailsResult;
|
||||
recentSearches: RecentSearchesResult;
|
||||
rules: RulesResult;
|
||||
|
|
@ -2445,6 +2565,18 @@ export type QueryIntegrationArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type QueryPostArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryPostsArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
userId: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryRulesArgs = {
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
|
@ -3824,6 +3956,33 @@ export type UpdatePageSuccess = {
|
|||
updatedPage: Article;
|
||||
};
|
||||
|
||||
export type UpdatePostError = {
|
||||
__typename?: 'UpdatePostError';
|
||||
errorCodes: Array<UpdatePostErrorCode>;
|
||||
};
|
||||
|
||||
export enum UpdatePostErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type UpdatePostInput = {
|
||||
content?: InputMaybe<Scalars['String']>;
|
||||
highlightIds?: InputMaybe<Array<Scalars['ID']>>;
|
||||
id: Scalars['ID'];
|
||||
libraryItemIds?: InputMaybe<Array<Scalars['ID']>>;
|
||||
thought?: InputMaybe<Scalars['String']>;
|
||||
thumbnail?: InputMaybe<Scalars['String']>;
|
||||
title?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UpdatePostResult = UpdatePostError | UpdatePostSuccess;
|
||||
|
||||
export type UpdatePostSuccess = {
|
||||
__typename?: 'UpdatePostSuccess';
|
||||
post: Post;
|
||||
};
|
||||
|
||||
export enum UpdateReason {
|
||||
Created = 'CREATED',
|
||||
Deleted = 'DELETED',
|
||||
|
|
@ -4325,6 +4484,11 @@ export type ResolversTypes = {
|
|||
CreateNewsletterEmailInput: CreateNewsletterEmailInput;
|
||||
CreateNewsletterEmailResult: ResolversTypes['CreateNewsletterEmailError'] | ResolversTypes['CreateNewsletterEmailSuccess'];
|
||||
CreateNewsletterEmailSuccess: ResolverTypeWrapper<CreateNewsletterEmailSuccess>;
|
||||
CreatePostError: ResolverTypeWrapper<CreatePostError>;
|
||||
CreatePostErrorCode: CreatePostErrorCode;
|
||||
CreatePostInput: CreatePostInput;
|
||||
CreatePostResult: ResolversTypes['CreatePostError'] | ResolversTypes['CreatePostSuccess'];
|
||||
CreatePostSuccess: ResolverTypeWrapper<CreatePostSuccess>;
|
||||
CreateReactionError: ResolverTypeWrapper<CreateReactionError>;
|
||||
CreateReactionErrorCode: CreateReactionErrorCode;
|
||||
CreateReactionInput: CreateReactionInput;
|
||||
|
|
@ -4378,6 +4542,10 @@ export type ResolversTypes = {
|
|||
DeleteNewsletterEmailErrorCode: DeleteNewsletterEmailErrorCode;
|
||||
DeleteNewsletterEmailResult: ResolversTypes['DeleteNewsletterEmailError'] | ResolversTypes['DeleteNewsletterEmailSuccess'];
|
||||
DeleteNewsletterEmailSuccess: ResolverTypeWrapper<DeleteNewsletterEmailSuccess>;
|
||||
DeletePostError: ResolverTypeWrapper<DeletePostError>;
|
||||
DeletePostErrorCode: DeletePostErrorCode;
|
||||
DeletePostResult: ResolversTypes['DeletePostError'] | ResolversTypes['DeletePostSuccess'];
|
||||
DeletePostSuccess: ResolverTypeWrapper<DeletePostSuccess>;
|
||||
DeleteReactionError: ResolverTypeWrapper<DeleteReactionError>;
|
||||
DeleteReactionErrorCode: DeleteReactionErrorCode;
|
||||
DeleteReactionResult: ResolversTypes['DeleteReactionError'] | ResolversTypes['DeleteReactionSuccess'];
|
||||
|
|
@ -4590,6 +4758,16 @@ export type ResolversTypes = {
|
|||
PageInfoInput: PageInfoInput;
|
||||
PageType: PageType;
|
||||
ParseResult: ParseResult;
|
||||
Post: ResolverTypeWrapper<Post>;
|
||||
PostEdge: ResolverTypeWrapper<PostEdge>;
|
||||
PostError: ResolverTypeWrapper<PostError>;
|
||||
PostErrorCode: PostErrorCode;
|
||||
PostResult: ResolversTypes['PostError'] | ResolversTypes['PostSuccess'];
|
||||
PostSuccess: ResolverTypeWrapper<PostSuccess>;
|
||||
PostsError: ResolverTypeWrapper<PostsError>;
|
||||
PostsErrorCode: PostsErrorCode;
|
||||
PostsResult: ResolversTypes['PostsError'] | ResolversTypes['PostsSuccess'];
|
||||
PostsSuccess: ResolverTypeWrapper<PostsSuccess>;
|
||||
PreparedDocumentInput: PreparedDocumentInput;
|
||||
Profile: ResolverTypeWrapper<Profile>;
|
||||
Query: ResolverTypeWrapper<{}>;
|
||||
|
|
@ -4824,6 +5002,11 @@ export type ResolversTypes = {
|
|||
UpdatePageInput: UpdatePageInput;
|
||||
UpdatePageResult: ResolversTypes['UpdatePageError'] | ResolversTypes['UpdatePageSuccess'];
|
||||
UpdatePageSuccess: ResolverTypeWrapper<UpdatePageSuccess>;
|
||||
UpdatePostError: ResolverTypeWrapper<UpdatePostError>;
|
||||
UpdatePostErrorCode: UpdatePostErrorCode;
|
||||
UpdatePostInput: UpdatePostInput;
|
||||
UpdatePostResult: ResolversTypes['UpdatePostError'] | ResolversTypes['UpdatePostSuccess'];
|
||||
UpdatePostSuccess: ResolverTypeWrapper<UpdatePostSuccess>;
|
||||
UpdateReason: UpdateReason;
|
||||
UpdateReminderError: ResolverTypeWrapper<UpdateReminderError>;
|
||||
UpdateReminderErrorCode: UpdateReminderErrorCode;
|
||||
|
|
@ -4953,6 +5136,10 @@ export type ResolversParentTypes = {
|
|||
CreateNewsletterEmailInput: CreateNewsletterEmailInput;
|
||||
CreateNewsletterEmailResult: ResolversParentTypes['CreateNewsletterEmailError'] | ResolversParentTypes['CreateNewsletterEmailSuccess'];
|
||||
CreateNewsletterEmailSuccess: CreateNewsletterEmailSuccess;
|
||||
CreatePostError: CreatePostError;
|
||||
CreatePostInput: CreatePostInput;
|
||||
CreatePostResult: ResolversParentTypes['CreatePostError'] | ResolversParentTypes['CreatePostSuccess'];
|
||||
CreatePostSuccess: CreatePostSuccess;
|
||||
CreateReactionError: CreateReactionError;
|
||||
CreateReactionInput: CreateReactionInput;
|
||||
CreateReactionResult: ResolversParentTypes['CreateReactionError'] | ResolversParentTypes['CreateReactionSuccess'];
|
||||
|
|
@ -4994,6 +5181,9 @@ export type ResolversParentTypes = {
|
|||
DeleteNewsletterEmailError: DeleteNewsletterEmailError;
|
||||
DeleteNewsletterEmailResult: ResolversParentTypes['DeleteNewsletterEmailError'] | ResolversParentTypes['DeleteNewsletterEmailSuccess'];
|
||||
DeleteNewsletterEmailSuccess: DeleteNewsletterEmailSuccess;
|
||||
DeletePostError: DeletePostError;
|
||||
DeletePostResult: ResolversParentTypes['DeletePostError'] | ResolversParentTypes['DeletePostSuccess'];
|
||||
DeletePostSuccess: DeletePostSuccess;
|
||||
DeleteReactionError: DeleteReactionError;
|
||||
DeleteReactionResult: ResolversParentTypes['DeleteReactionError'] | ResolversParentTypes['DeleteReactionSuccess'];
|
||||
DeleteReactionSuccess: DeleteReactionSuccess;
|
||||
|
|
@ -5158,6 +5348,14 @@ export type ResolversParentTypes = {
|
|||
PageInfo: PageInfo;
|
||||
PageInfoInput: PageInfoInput;
|
||||
ParseResult: ParseResult;
|
||||
Post: Post;
|
||||
PostEdge: PostEdge;
|
||||
PostError: PostError;
|
||||
PostResult: ResolversParentTypes['PostError'] | ResolversParentTypes['PostSuccess'];
|
||||
PostSuccess: PostSuccess;
|
||||
PostsError: PostsError;
|
||||
PostsResult: ResolversParentTypes['PostsError'] | ResolversParentTypes['PostsSuccess'];
|
||||
PostsSuccess: PostsSuccess;
|
||||
PreparedDocumentInput: PreparedDocumentInput;
|
||||
Profile: Profile;
|
||||
Query: {};
|
||||
|
|
@ -5340,6 +5538,10 @@ export type ResolversParentTypes = {
|
|||
UpdatePageInput: UpdatePageInput;
|
||||
UpdatePageResult: ResolversParentTypes['UpdatePageError'] | ResolversParentTypes['UpdatePageSuccess'];
|
||||
UpdatePageSuccess: UpdatePageSuccess;
|
||||
UpdatePostError: UpdatePostError;
|
||||
UpdatePostInput: UpdatePostInput;
|
||||
UpdatePostResult: ResolversParentTypes['UpdatePostError'] | ResolversParentTypes['UpdatePostSuccess'];
|
||||
UpdatePostSuccess: UpdatePostSuccess;
|
||||
UpdateReminderError: UpdateReminderError;
|
||||
UpdateReminderInput: UpdateReminderInput;
|
||||
UpdateReminderResult: ResolversParentTypes['UpdateReminderError'] | ResolversParentTypes['UpdateReminderSuccess'];
|
||||
|
|
@ -5703,6 +5905,20 @@ export type CreateNewsletterEmailSuccessResolvers<ContextType = ResolverContext,
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CreatePostErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreatePostError'] = ResolversParentTypes['CreatePostError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['CreatePostErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CreatePostResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreatePostResult'] = ResolversParentTypes['CreatePostResult']> = {
|
||||
__resolveType: TypeResolveFn<'CreatePostError' | 'CreatePostSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CreatePostSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreatePostSuccess'] = ResolversParentTypes['CreatePostSuccess']> = {
|
||||
post?: Resolver<ResolversTypes['Post'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CreateReactionErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['CreateReactionError'] = ResolversParentTypes['CreateReactionError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['CreateReactionErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
|
@ -5875,6 +6091,20 @@ export type DeleteNewsletterEmailSuccessResolvers<ContextType = ResolverContext,
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type DeletePostErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeletePostError'] = ResolversParentTypes['DeletePostError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['DeletePostErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type DeletePostResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeletePostResult'] = ResolversParentTypes['DeletePostResult']> = {
|
||||
__resolveType: TypeResolveFn<'DeletePostError' | 'DeletePostSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type DeletePostSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeletePostSuccess'] = ResolversParentTypes['DeletePostSuccess']> = {
|
||||
success?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type DeleteReactionErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['DeleteReactionError'] = ResolversParentTypes['DeleteReactionError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['DeleteReactionErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
|
@ -6694,6 +6924,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
|
|||
createHighlight?: Resolver<ResolversTypes['CreateHighlightResult'], ParentType, ContextType, RequireFields<MutationCreateHighlightArgs, 'input'>>;
|
||||
createLabel?: Resolver<ResolversTypes['CreateLabelResult'], ParentType, ContextType, RequireFields<MutationCreateLabelArgs, 'input'>>;
|
||||
createNewsletterEmail?: Resolver<ResolversTypes['CreateNewsletterEmailResult'], ParentType, ContextType, Partial<MutationCreateNewsletterEmailArgs>>;
|
||||
createPost?: Resolver<ResolversTypes['CreatePostResult'], ParentType, ContextType, RequireFields<MutationCreatePostArgs, 'input'>>;
|
||||
deleteAccount?: Resolver<ResolversTypes['DeleteAccountResult'], ParentType, ContextType, RequireFields<MutationDeleteAccountArgs, 'userID'>>;
|
||||
deleteDiscoverArticle?: Resolver<ResolversTypes['DeleteDiscoverArticleResult'], ParentType, ContextType, RequireFields<MutationDeleteDiscoverArticleArgs, 'input'>>;
|
||||
deleteDiscoverFeed?: Resolver<ResolversTypes['DeleteDiscoverFeedResult'], ParentType, ContextType, RequireFields<MutationDeleteDiscoverFeedArgs, 'input'>>;
|
||||
|
|
@ -6703,6 +6934,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
|
|||
deleteIntegration?: Resolver<ResolversTypes['DeleteIntegrationResult'], ParentType, ContextType, RequireFields<MutationDeleteIntegrationArgs, 'id'>>;
|
||||
deleteLabel?: Resolver<ResolversTypes['DeleteLabelResult'], ParentType, ContextType, RequireFields<MutationDeleteLabelArgs, 'id'>>;
|
||||
deleteNewsletterEmail?: Resolver<ResolversTypes['DeleteNewsletterEmailResult'], ParentType, ContextType, RequireFields<MutationDeleteNewsletterEmailArgs, 'newsletterEmailId'>>;
|
||||
deletePost?: Resolver<ResolversTypes['DeletePostResult'], ParentType, ContextType, RequireFields<MutationDeletePostArgs, 'id'>>;
|
||||
deleteRule?: Resolver<ResolversTypes['DeleteRuleResult'], ParentType, ContextType, RequireFields<MutationDeleteRuleArgs, 'id'>>;
|
||||
deleteWebhook?: Resolver<ResolversTypes['DeleteWebhookResult'], ParentType, ContextType, RequireFields<MutationDeleteWebhookArgs, 'id'>>;
|
||||
editDiscoverFeed?: Resolver<ResolversTypes['EditDiscoverFeedResult'], ParentType, ContextType, RequireFields<MutationEditDiscoverFeedArgs, 'input'>>;
|
||||
|
|
@ -6753,6 +6985,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
|
|||
updateLabel?: Resolver<ResolversTypes['UpdateLabelResult'], ParentType, ContextType, RequireFields<MutationUpdateLabelArgs, 'input'>>;
|
||||
updateNewsletterEmail?: Resolver<ResolversTypes['UpdateNewsletterEmailResult'], ParentType, ContextType, RequireFields<MutationUpdateNewsletterEmailArgs, 'input'>>;
|
||||
updatePage?: Resolver<ResolversTypes['UpdatePageResult'], ParentType, ContextType, RequireFields<MutationUpdatePageArgs, 'input'>>;
|
||||
updatePost?: Resolver<ResolversTypes['UpdatePostResult'], ParentType, ContextType, RequireFields<MutationUpdatePostArgs, 'input'>>;
|
||||
updateSubscription?: Resolver<ResolversTypes['UpdateSubscriptionResult'], ParentType, ContextType, RequireFields<MutationUpdateSubscriptionArgs, 'input'>>;
|
||||
updateUser?: Resolver<ResolversTypes['UpdateUserResult'], ParentType, ContextType, RequireFields<MutationUpdateUserArgs, 'input'>>;
|
||||
updateUserProfile?: Resolver<ResolversTypes['UpdateUserProfileResult'], ParentType, ContextType, RequireFields<MutationUpdateUserProfileArgs, 'input'>>;
|
||||
|
|
@ -6827,6 +7060,56 @@ export type PageInfoResolvers<ContextType = ResolverContext, ParentType extends
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type PostResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Post'] = ResolversParentTypes['Post']> = {
|
||||
author?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
content?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
createdAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
highlights?: Resolver<Maybe<Array<ResolversTypes['Highlight']>>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
libraryItems?: Resolver<Maybe<Array<ResolversTypes['Article']>>, ParentType, ContextType>;
|
||||
ownedByViewer?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
thought?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
thumbnail?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
updatedAt?: Resolver<ResolversTypes['Date'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type PostEdgeResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['PostEdge'] = ResolversParentTypes['PostEdge']> = {
|
||||
cursor?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
node?: Resolver<ResolversTypes['Post'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type PostErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['PostError'] = ResolversParentTypes['PostError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['PostErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type PostResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['PostResult'] = ResolversParentTypes['PostResult']> = {
|
||||
__resolveType: TypeResolveFn<'PostError' | 'PostSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type PostSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['PostSuccess'] = ResolversParentTypes['PostSuccess']> = {
|
||||
post?: Resolver<ResolversTypes['Post'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type PostsErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['PostsError'] = ResolversParentTypes['PostsError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['PostsErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type PostsResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['PostsResult'] = ResolversParentTypes['PostsResult']> = {
|
||||
__resolveType: TypeResolveFn<'PostsError' | 'PostsSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type PostsSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['PostsSuccess'] = ResolversParentTypes['PostsSuccess']> = {
|
||||
edges?: Resolver<Array<ResolversTypes['PostEdge']>, ParentType, ContextType>;
|
||||
pageInfo?: Resolver<ResolversTypes['PageInfo'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type ProfileResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['Profile'] = ResolversParentTypes['Profile']> = {
|
||||
bio?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
|
|
@ -6858,6 +7141,8 @@ export type QueryResolvers<ContextType = ResolverContext, ParentType extends Res
|
|||
labels?: Resolver<ResolversTypes['LabelsResult'], ParentType, ContextType>;
|
||||
me?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;
|
||||
newsletterEmails?: Resolver<ResolversTypes['NewsletterEmailsResult'], ParentType, ContextType>;
|
||||
post?: Resolver<ResolversTypes['PostResult'], ParentType, ContextType, RequireFields<QueryPostArgs, 'id'>>;
|
||||
posts?: Resolver<ResolversTypes['PostsResult'], ParentType, ContextType, RequireFields<QueryPostsArgs, 'userId'>>;
|
||||
recentEmails?: Resolver<ResolversTypes['RecentEmailsResult'], ParentType, ContextType>;
|
||||
recentSearches?: Resolver<ResolversTypes['RecentSearchesResult'], ParentType, ContextType>;
|
||||
rules?: Resolver<ResolversTypes['RulesResult'], ParentType, ContextType, Partial<QueryRulesArgs>>;
|
||||
|
|
@ -7696,6 +7981,20 @@ export type UpdatePageSuccessResolvers<ContextType = ResolverContext, ParentType
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdatePostErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdatePostError'] = ResolversParentTypes['UpdatePostError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['UpdatePostErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdatePostResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdatePostResult'] = ResolversParentTypes['UpdatePostResult']> = {
|
||||
__resolveType: TypeResolveFn<'UpdatePostError' | 'UpdatePostSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdatePostSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdatePostSuccess'] = ResolversParentTypes['UpdatePostSuccess']> = {
|
||||
post?: Resolver<ResolversTypes['Post'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdateReminderErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateReminderError'] = ResolversParentTypes['UpdateReminderError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['UpdateReminderErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
|
@ -7974,6 +8273,9 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
CreateNewsletterEmailError?: CreateNewsletterEmailErrorResolvers<ContextType>;
|
||||
CreateNewsletterEmailResult?: CreateNewsletterEmailResultResolvers<ContextType>;
|
||||
CreateNewsletterEmailSuccess?: CreateNewsletterEmailSuccessResolvers<ContextType>;
|
||||
CreatePostError?: CreatePostErrorResolvers<ContextType>;
|
||||
CreatePostResult?: CreatePostResultResolvers<ContextType>;
|
||||
CreatePostSuccess?: CreatePostSuccessResolvers<ContextType>;
|
||||
CreateReactionError?: CreateReactionErrorResolvers<ContextType>;
|
||||
CreateReactionResult?: CreateReactionResultResolvers<ContextType>;
|
||||
CreateReactionSuccess?: CreateReactionSuccessResolvers<ContextType>;
|
||||
|
|
@ -8011,6 +8313,9 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
DeleteNewsletterEmailError?: DeleteNewsletterEmailErrorResolvers<ContextType>;
|
||||
DeleteNewsletterEmailResult?: DeleteNewsletterEmailResultResolvers<ContextType>;
|
||||
DeleteNewsletterEmailSuccess?: DeleteNewsletterEmailSuccessResolvers<ContextType>;
|
||||
DeletePostError?: DeletePostErrorResolvers<ContextType>;
|
||||
DeletePostResult?: DeletePostResultResolvers<ContextType>;
|
||||
DeletePostSuccess?: DeletePostSuccessResolvers<ContextType>;
|
||||
DeleteReactionError?: DeleteReactionErrorResolvers<ContextType>;
|
||||
DeleteReactionResult?: DeleteReactionResultResolvers<ContextType>;
|
||||
DeleteReactionSuccess?: DeleteReactionSuccessResolvers<ContextType>;
|
||||
|
|
@ -8160,6 +8465,14 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
OptInFeatureSuccess?: OptInFeatureSuccessResolvers<ContextType>;
|
||||
Page?: PageResolvers<ContextType>;
|
||||
PageInfo?: PageInfoResolvers<ContextType>;
|
||||
Post?: PostResolvers<ContextType>;
|
||||
PostEdge?: PostEdgeResolvers<ContextType>;
|
||||
PostError?: PostErrorResolvers<ContextType>;
|
||||
PostResult?: PostResultResolvers<ContextType>;
|
||||
PostSuccess?: PostSuccessResolvers<ContextType>;
|
||||
PostsError?: PostsErrorResolvers<ContextType>;
|
||||
PostsResult?: PostsResultResolvers<ContextType>;
|
||||
PostsSuccess?: PostsSuccessResolvers<ContextType>;
|
||||
Profile?: ProfileResolvers<ContextType>;
|
||||
Query?: QueryResolvers<ContextType>;
|
||||
Reaction?: ReactionResolvers<ContextType>;
|
||||
|
|
@ -8307,6 +8620,9 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
UpdatePageError?: UpdatePageErrorResolvers<ContextType>;
|
||||
UpdatePageResult?: UpdatePageResultResolvers<ContextType>;
|
||||
UpdatePageSuccess?: UpdatePageSuccessResolvers<ContextType>;
|
||||
UpdatePostError?: UpdatePostErrorResolvers<ContextType>;
|
||||
UpdatePostResult?: UpdatePostResultResolvers<ContextType>;
|
||||
UpdatePostSuccess?: UpdatePostSuccessResolvers<ContextType>;
|
||||
UpdateReminderError?: UpdateReminderErrorResolvers<ContextType>;
|
||||
UpdateReminderResult?: UpdateReminderResultResolvers<ContextType>;
|
||||
UpdateReminderSuccess?: UpdateReminderSuccessResolvers<ContextType>;
|
||||
|
|
|
|||
|
|
@ -455,6 +455,30 @@ type CreateNewsletterEmailSuccess {
|
|||
newsletterEmail: NewsletterEmail!
|
||||
}
|
||||
|
||||
type CreatePostError {
|
||||
errorCodes: [CreatePostErrorCode!]!
|
||||
}
|
||||
|
||||
enum CreatePostErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input CreatePostInput {
|
||||
content: String!
|
||||
highlightIds: [ID!]
|
||||
libraryItemIds: [ID!]!
|
||||
thought: String
|
||||
thumbnail: String
|
||||
title: String!
|
||||
}
|
||||
|
||||
union CreatePostResult = CreatePostError | CreatePostSuccess
|
||||
|
||||
type CreatePostSuccess {
|
||||
post: Post!
|
||||
}
|
||||
|
||||
type CreateReactionError {
|
||||
errorCodes: [CreateReactionErrorCode!]!
|
||||
}
|
||||
|
|
@ -673,6 +697,21 @@ type DeleteNewsletterEmailSuccess {
|
|||
newsletterEmail: NewsletterEmail!
|
||||
}
|
||||
|
||||
type DeletePostError {
|
||||
errorCodes: [DeletePostErrorCode!]!
|
||||
}
|
||||
|
||||
enum DeletePostErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union DeletePostResult = DeletePostError | DeletePostSuccess
|
||||
|
||||
type DeletePostSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type DeleteReactionError {
|
||||
errorCodes: [DeleteReactionErrorCode!]!
|
||||
}
|
||||
|
|
@ -1625,6 +1664,7 @@ type Mutation {
|
|||
createHighlight(input: CreateHighlightInput!): CreateHighlightResult!
|
||||
createLabel(input: CreateLabelInput!): CreateLabelResult!
|
||||
createNewsletterEmail(input: CreateNewsletterEmailInput): CreateNewsletterEmailResult!
|
||||
createPost(input: CreatePostInput!): CreatePostResult!
|
||||
deleteAccount(userID: ID!): DeleteAccountResult!
|
||||
deleteDiscoverArticle(input: DeleteDiscoverArticleInput!): DeleteDiscoverArticleResult!
|
||||
deleteDiscoverFeed(input: DeleteDiscoverFeedInput!): DeleteDiscoverFeedResult!
|
||||
|
|
@ -1634,6 +1674,7 @@ type Mutation {
|
|||
deleteIntegration(id: ID!): DeleteIntegrationResult!
|
||||
deleteLabel(id: ID!): DeleteLabelResult!
|
||||
deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult!
|
||||
deletePost(id: ID!): DeletePostResult!
|
||||
deleteRule(id: ID!): DeleteRuleResult!
|
||||
deleteWebhook(id: ID!): DeleteWebhookResult!
|
||||
editDiscoverFeed(input: EditDiscoverFeedInput!): EditDiscoverFeedResult!
|
||||
|
|
@ -1684,6 +1725,7 @@ type Mutation {
|
|||
updateLabel(input: UpdateLabelInput!): UpdateLabelResult!
|
||||
updateNewsletterEmail(input: UpdateNewsletterEmailInput!): UpdateNewsletterEmailResult!
|
||||
updatePage(input: UpdatePageInput!): UpdatePageResult!
|
||||
updatePost(input: UpdatePostInput!): UpdatePostResult!
|
||||
updateSubscription(input: UpdateSubscriptionInput!): UpdateSubscriptionResult!
|
||||
updateUser(input: UpdateUserInput!): UpdateUserResult!
|
||||
updateUserProfile(input: UpdateUserProfileInput!): UpdateUserProfileResult!
|
||||
|
|
@ -1800,6 +1842,57 @@ input ParseResult {
|
|||
title: String!
|
||||
}
|
||||
|
||||
type Post {
|
||||
author: String!
|
||||
content: String!
|
||||
createdAt: Date!
|
||||
highlights: [Highlight!]
|
||||
id: ID!
|
||||
libraryItems: [Article!]
|
||||
ownedByViewer: Boolean!
|
||||
thought: String
|
||||
thumbnail: String
|
||||
title: String!
|
||||
updatedAt: Date!
|
||||
}
|
||||
|
||||
type PostEdge {
|
||||
cursor: String!
|
||||
node: Post!
|
||||
}
|
||||
|
||||
type PostError {
|
||||
errorCodes: [PostErrorCode!]!
|
||||
}
|
||||
|
||||
enum PostErrorCode {
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union PostResult = PostError | PostSuccess
|
||||
|
||||
type PostSuccess {
|
||||
post: Post!
|
||||
}
|
||||
|
||||
type PostsError {
|
||||
errorCodes: [PostsErrorCode!]!
|
||||
}
|
||||
|
||||
enum PostsErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
union PostsResult = PostsError | PostsSuccess
|
||||
|
||||
type PostsSuccess {
|
||||
edges: [PostEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
input PreparedDocumentInput {
|
||||
document: String!
|
||||
pageInfo: PageInfoInput!
|
||||
|
|
@ -1835,6 +1928,8 @@ type Query {
|
|||
labels: LabelsResult!
|
||||
me: User
|
||||
newsletterEmails: NewsletterEmailsResult!
|
||||
post(id: ID!): PostResult!
|
||||
posts(after: String, first: Int, userId: ID!): PostsResult!
|
||||
recentEmails: RecentEmailsResult!
|
||||
recentSearches: RecentSearchesResult!
|
||||
rules(enabled: Boolean): RulesResult!
|
||||
|
|
@ -3068,6 +3163,31 @@ type UpdatePageSuccess {
|
|||
updatedPage: Article!
|
||||
}
|
||||
|
||||
type UpdatePostError {
|
||||
errorCodes: [UpdatePostErrorCode!]!
|
||||
}
|
||||
|
||||
enum UpdatePostErrorCode {
|
||||
BAD_REQUEST
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input UpdatePostInput {
|
||||
content: String
|
||||
highlightIds: [ID!]
|
||||
id: ID!
|
||||
libraryItemIds: [ID!]
|
||||
thought: String
|
||||
thumbnail: String
|
||||
title: String
|
||||
}
|
||||
|
||||
union UpdatePostResult = UpdatePostError | UpdatePostSuccess
|
||||
|
||||
type UpdatePostSuccess {
|
||||
post: Post!
|
||||
}
|
||||
|
||||
enum UpdateReason {
|
||||
CREATED
|
||||
DELETED
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const authTrx = async <T>(
|
|||
): Promise<T> => {
|
||||
let { uid, userRole } = options
|
||||
|
||||
// if uid and dbRole are not passed in, then get them from the claims
|
||||
// if uid and dbRole are not passed in, then get them from the http context
|
||||
if (!uid && !userRole) {
|
||||
const claims: Claims | undefined = httpContext.get('claims')
|
||||
uid = claims?.uid
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
EXISTING_NEWSLETTER_FOLDER,
|
||||
NewsletterEmail,
|
||||
} from '../entity/newsletter_email'
|
||||
import { Post } from '../entity/post'
|
||||
import { PublicItem } from '../entity/public_item'
|
||||
import { Recommendation } from '../entity/recommendation'
|
||||
import {
|
||||
|
|
@ -151,6 +152,13 @@ import {
|
|||
webhookResolver,
|
||||
webhooksResolver,
|
||||
} from './index'
|
||||
import {
|
||||
createPostResolver,
|
||||
deletePostResolver,
|
||||
postResolver,
|
||||
postsResolver,
|
||||
updatePostResolver,
|
||||
} from './posts'
|
||||
import {
|
||||
markEmailAsItemResolver,
|
||||
recentEmailsResolver,
|
||||
|
|
@ -317,6 +325,9 @@ export const functionResolvers = {
|
|||
createFolderPolicy: createFolderPolicyResolver,
|
||||
updateFolderPolicy: updateFolderPolicyResolver,
|
||||
deleteFolderPolicy: deleteFolderPolicyResolver,
|
||||
createPost: createPostResolver,
|
||||
updatePost: updatePostResolver,
|
||||
deletePost: deletePostResolver,
|
||||
},
|
||||
Query: {
|
||||
me: getMeUserResolver,
|
||||
|
|
@ -353,6 +364,8 @@ export const functionResolvers = {
|
|||
hiddenHomeSection: hiddenHomeSectionResolver,
|
||||
highlights: highlightsResolver,
|
||||
folderPolicies: folderPoliciesResolver,
|
||||
posts: postsResolver,
|
||||
post: postResolver,
|
||||
},
|
||||
User: {
|
||||
async intercomHash(user: User) {
|
||||
|
|
@ -792,6 +805,35 @@ export const functionResolvers = {
|
|||
name: (recommendation: Recommendation) => recommendation.group.name,
|
||||
recommendedAt: (recommendation: Recommendation) => recommendation.createdAt,
|
||||
},
|
||||
Post: {
|
||||
async author(post: Post, _: never, ctx: ResolverContext) {
|
||||
const author = await ctx.dataLoaders.users.load(post.userId)
|
||||
return author?.name
|
||||
},
|
||||
ownedByViewer(post: Post, _: never, ctx: ResolverContext) {
|
||||
return post.userId === ctx.claims?.uid
|
||||
},
|
||||
async libraryItems(
|
||||
post: { libraryItemIds: string[] },
|
||||
_: never,
|
||||
ctx: ResolverContext
|
||||
) {
|
||||
const items = await ctx.dataLoaders.libraryItems.loadMany(
|
||||
post.libraryItemIds
|
||||
)
|
||||
return items.filter((item) => !!item)
|
||||
},
|
||||
async highlights(
|
||||
post: { highlightIds: string[] },
|
||||
_: never,
|
||||
ctx: ResolverContext
|
||||
) {
|
||||
const highlights = await ctx.dataLoaders.highlights.loadMany(
|
||||
post.highlightIds
|
||||
)
|
||||
return highlights.filter((highlight) => !!highlight)
|
||||
},
|
||||
},
|
||||
...resultResolveTypeResolver('Login'),
|
||||
...resultResolveTypeResolver('LogOut'),
|
||||
...resultResolveTypeResolver('GoogleSignup'),
|
||||
|
|
@ -890,4 +932,9 @@ export const functionResolvers = {
|
|||
...resultResolveTypeResolver('CreateFolderPolicy'),
|
||||
...resultResolveTypeResolver('UpdateFolderPolicy'),
|
||||
...resultResolveTypeResolver('DeleteFolderPolicy'),
|
||||
...resultResolveTypeResolver('Posts'),
|
||||
...resultResolveTypeResolver('Post'),
|
||||
...resultResolveTypeResolver('CreatePost'),
|
||||
...resultResolveTypeResolver('UpdatePost'),
|
||||
...resultResolveTypeResolver('DeletePost'),
|
||||
}
|
||||
|
|
|
|||
236
packages/api/src/resolvers/posts/index.ts
Normal file
236
packages/api/src/resolvers/posts/index.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import { Post } from '../../entity/post'
|
||||
import {
|
||||
CreatePostError,
|
||||
CreatePostErrorCode,
|
||||
CreatePostSuccess,
|
||||
DeletePostError,
|
||||
DeletePostErrorCode,
|
||||
DeletePostSuccess,
|
||||
MutationCreatePostArgs,
|
||||
MutationDeletePostArgs,
|
||||
MutationUpdatePostArgs,
|
||||
PostEdge,
|
||||
PostErrorCode,
|
||||
PostResult,
|
||||
PostsErrorCode,
|
||||
PostsResult,
|
||||
QueryPostArgs,
|
||||
QueryPostsArgs,
|
||||
ResolverFn,
|
||||
UpdatePostError,
|
||||
UpdatePostErrorCode,
|
||||
UpdatePostSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import {
|
||||
createPublicPost,
|
||||
deletePosts,
|
||||
findPublicPostById,
|
||||
findPublicPostsByUserId,
|
||||
updatePost,
|
||||
} from '../../services/post'
|
||||
import { Merge } from '../../util'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
import { ResolverContext } from '../types'
|
||||
|
||||
type PartialPostEdge = Merge<
|
||||
PostEdge,
|
||||
{
|
||||
node: Post
|
||||
}
|
||||
>
|
||||
type PartialPostsResult = Merge<
|
||||
PostsResult,
|
||||
{
|
||||
edges?: Array<PartialPostEdge>
|
||||
}
|
||||
>
|
||||
export const postsResolver: ResolverFn<
|
||||
PartialPostsResult,
|
||||
never,
|
||||
ResolverContext,
|
||||
QueryPostsArgs
|
||||
> = async (_, { first, after, userId }, { log }) => {
|
||||
const limit = first || 10
|
||||
const offset = parseInt(after || '0')
|
||||
if (isNaN(offset) || offset < 0 || limit > 50) {
|
||||
log.error('Invalid args', { after, first })
|
||||
|
||||
return {
|
||||
errorCodes: [PostsErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
const posts = await findPublicPostsByUserId(userId, limit + 1, offset)
|
||||
|
||||
const hasNextPage = posts.length > limit
|
||||
if (hasNextPage) {
|
||||
posts.pop()
|
||||
}
|
||||
const endCursor = String(offset + posts.length)
|
||||
|
||||
const edges = posts.map((post) => ({
|
||||
cursor: endCursor,
|
||||
node: post,
|
||||
}))
|
||||
|
||||
return {
|
||||
edges,
|
||||
pageInfo: {
|
||||
startCursor: String(offset),
|
||||
endCursor,
|
||||
hasPreviousPage: offset > 0,
|
||||
hasNextPage,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const postResolver: ResolverFn<
|
||||
Merge<PostResult, { post?: Post }>,
|
||||
never,
|
||||
ResolverContext,
|
||||
QueryPostArgs
|
||||
> = async (_, { id }, { log }) => {
|
||||
if (!id) {
|
||||
log.error('Invalid args', { id })
|
||||
|
||||
return {
|
||||
errorCodes: [PostErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
const post = await findPublicPostById(id)
|
||||
|
||||
if (!post) {
|
||||
log.error('Post not found', { id })
|
||||
|
||||
return {
|
||||
errorCodes: [PostErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
post,
|
||||
}
|
||||
}
|
||||
|
||||
export const createPostResolver = authorized<
|
||||
Merge<CreatePostSuccess, { post: Post }>,
|
||||
CreatePostError,
|
||||
MutationCreatePostArgs
|
||||
>(async (_, { input }, { uid, log }) => {
|
||||
const { title, content, highlightIds, libraryItemIds, thought, thumbnail } =
|
||||
input
|
||||
|
||||
if (libraryItemIds.length === 0) {
|
||||
log.error('Invalid args', { libraryItemIds })
|
||||
|
||||
return {
|
||||
errorCodes: [CreatePostErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
const postToCreate = {
|
||||
userId: uid,
|
||||
title,
|
||||
content,
|
||||
libraryItemIds,
|
||||
highlightIds: highlightIds || undefined,
|
||||
thought: thought || undefined,
|
||||
thumbnail: thumbnail || undefined,
|
||||
}
|
||||
|
||||
const post = await createPublicPost(uid, postToCreate)
|
||||
if (!post) {
|
||||
log.error('Failed to create post', { postToCreate })
|
||||
|
||||
return {
|
||||
errorCodes: [CreatePostErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
post,
|
||||
}
|
||||
})
|
||||
|
||||
export const updatePostResolver = authorized<
|
||||
Merge<UpdatePostSuccess, { post?: Post }>,
|
||||
UpdatePostError,
|
||||
MutationUpdatePostArgs
|
||||
>(async (_, { input }, { uid, log }) => {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
content,
|
||||
highlightIds,
|
||||
libraryItemIds,
|
||||
thought,
|
||||
thumbnail,
|
||||
} = input
|
||||
|
||||
if (!id || title === null || content === null || libraryItemIds === null) {
|
||||
log.error('Invalid args', { id })
|
||||
|
||||
return {
|
||||
errorCodes: [UpdatePostErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
const result = await updatePost(uid, id, {
|
||||
title,
|
||||
content,
|
||||
highlightIds,
|
||||
libraryItemIds,
|
||||
thought,
|
||||
thumbnail,
|
||||
})
|
||||
|
||||
if (!result.affected) {
|
||||
log.error('Failed to update post', { id })
|
||||
|
||||
return {
|
||||
errorCodes: [UpdatePostErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const post = await findPublicPostById(id)
|
||||
if (!post) {
|
||||
log.error('Post not found', { id })
|
||||
|
||||
return {
|
||||
errorCodes: [UpdatePostErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
post,
|
||||
}
|
||||
})
|
||||
|
||||
export const deletePostResolver = authorized<
|
||||
DeletePostSuccess,
|
||||
DeletePostError,
|
||||
MutationDeletePostArgs
|
||||
>(async (_, { id }, { uid, log }) => {
|
||||
if (!id) {
|
||||
log.error('Invalid args', { id })
|
||||
|
||||
return {
|
||||
errorCodes: [DeletePostErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
const result = await deletePosts(uid, [id])
|
||||
|
||||
if (!result.affected) {
|
||||
log.error('Failed to delete post', { id })
|
||||
|
||||
return {
|
||||
errorCodes: [DeletePostErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
})
|
||||
|
|
@ -3340,6 +3340,121 @@ const schema = gql`
|
|||
afterDays: Int
|
||||
}
|
||||
|
||||
type Post {
|
||||
id: ID!
|
||||
title: String!
|
||||
content: String!
|
||||
author: String!
|
||||
ownedByViewer: Boolean!
|
||||
thumbnail: String
|
||||
thought: String
|
||||
libraryItems: [Article!]
|
||||
highlights: [Highlight!]
|
||||
createdAt: Date!
|
||||
updatedAt: Date!
|
||||
}
|
||||
|
||||
input CreatePostInput {
|
||||
title: String! @sanitize(minLength: 1, maxLength: 255)
|
||||
content: String! @sanitize(minLength: 1)
|
||||
thumbnail: String
|
||||
libraryItemIds: [ID!]!
|
||||
highlightIds: [ID!]
|
||||
thought: String
|
||||
}
|
||||
|
||||
union CreatePostResult = CreatePostSuccess | CreatePostError
|
||||
|
||||
type CreatePostSuccess {
|
||||
post: Post!
|
||||
}
|
||||
|
||||
type CreatePostError {
|
||||
errorCodes: [CreatePostErrorCode!]!
|
||||
}
|
||||
|
||||
enum CreatePostErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
input UpdatePostInput {
|
||||
id: ID!
|
||||
title: String @sanitize(minLength: 1, maxLength: 255)
|
||||
content: String @sanitize(minLength: 1)
|
||||
thumbnail: String
|
||||
libraryItemIds: [ID!]
|
||||
highlightIds: [ID!]
|
||||
thought: String
|
||||
}
|
||||
|
||||
union UpdatePostResult = UpdatePostSuccess | UpdatePostError
|
||||
|
||||
type UpdatePostSuccess {
|
||||
post: Post!
|
||||
}
|
||||
|
||||
type UpdatePostError {
|
||||
errorCodes: [UpdatePostErrorCode!]!
|
||||
}
|
||||
|
||||
enum UpdatePostErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
union DeletePostResult = DeletePostSuccess | DeletePostError
|
||||
|
||||
type DeletePostSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type DeletePostError {
|
||||
errorCodes: [DeletePostErrorCode!]!
|
||||
}
|
||||
|
||||
enum DeletePostErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
union PostsResult = PostsSuccess | PostsError
|
||||
|
||||
type PostsSuccess {
|
||||
edges: [PostEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type PostEdge {
|
||||
cursor: String!
|
||||
node: Post!
|
||||
}
|
||||
|
||||
type PostsError {
|
||||
errorCodes: [PostsErrorCode!]!
|
||||
}
|
||||
|
||||
enum PostsErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
}
|
||||
|
||||
union PostResult = PostSuccess | PostError
|
||||
|
||||
type PostSuccess {
|
||||
post: Post!
|
||||
}
|
||||
|
||||
type PostError {
|
||||
errorCodes: [PostErrorCode!]!
|
||||
}
|
||||
|
||||
enum PostErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
}
|
||||
|
||||
# Mutations
|
||||
type Mutation {
|
||||
googleLogin(input: GoogleLoginInput!): LoginResult!
|
||||
|
|
@ -3354,24 +3469,10 @@ const schema = gql`
|
|||
mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult!
|
||||
updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult!
|
||||
deleteHighlight(highlightId: ID!): DeleteHighlightResult!
|
||||
# createHighlightReply(
|
||||
# input: CreateHighlightReplyInput!
|
||||
# ): CreateHighlightReplyResult!
|
||||
# updateHighlightReply(
|
||||
# input: UpdateHighlightReplyInput!
|
||||
# ): UpdateHighlightReplyResult!
|
||||
# deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult!
|
||||
# createReaction(input: CreateReactionInput!): CreateReactionResult!
|
||||
# deleteReaction(id: ID!): DeleteReactionResult!
|
||||
uploadFileRequest(input: UploadFileRequestInput!): UploadFileRequestResult!
|
||||
saveArticleReadingProgress(
|
||||
input: SaveArticleReadingProgressInput!
|
||||
): SaveArticleReadingProgressResult!
|
||||
# setShareArticle(input: SetShareArticleInput!): SetShareArticleResult!
|
||||
# updateSharedComment(
|
||||
# input: UpdateSharedCommentInput!
|
||||
# ): UpdateSharedCommentResult!
|
||||
# setFollow(input: SetFollowInput!): SetFollowResult!
|
||||
setBookmarkArticle(
|
||||
input: SetBookmarkArticleInput!
|
||||
): SetBookmarkArticleResult!
|
||||
|
|
@ -3381,11 +3482,7 @@ const schema = gql`
|
|||
createArticleSavingRequest(
|
||||
input: CreateArticleSavingRequestInput!
|
||||
): CreateArticleSavingRequestResult!
|
||||
# setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult!
|
||||
reportItem(input: ReportItemInput!): ReportItemResult!
|
||||
# updateLinkShareInfo(
|
||||
# input: UpdateLinkShareInfoInput!
|
||||
# ): UpdateLinkShareInfoResult!
|
||||
setLinkArchived(input: ArchiveLinkInput!): ArchiveLinkResult!
|
||||
createNewsletterEmail(
|
||||
input: CreateNewsletterEmailInput
|
||||
|
|
@ -3473,6 +3570,9 @@ const schema = gql`
|
|||
input: UpdateFolderPolicyInput!
|
||||
): UpdateFolderPolicyResult!
|
||||
deleteFolderPolicy(id: ID!): DeleteFolderPolicyResult!
|
||||
createPost(input: CreatePostInput!): CreatePostResult!
|
||||
updatePost(input: UpdatePostInput!): UpdatePostResult!
|
||||
deletePost(id: ID!): DeletePostResult!
|
||||
}
|
||||
|
||||
# FIXME: remove sort from feedArticles after all cached tabs are closed
|
||||
|
|
@ -3482,17 +3582,6 @@ const schema = gql`
|
|||
me: User
|
||||
user(userId: ID, username: String): UserResult!
|
||||
article(username: String!, slug: String!, format: String): ArticleResult!
|
||||
# sharedArticle(
|
||||
# username: String!
|
||||
# slug: String!
|
||||
# selectedHighlightId: String
|
||||
# ): SharedArticleResult!
|
||||
# feedArticles(
|
||||
# after: String
|
||||
# first: Int
|
||||
# sort: SortParams
|
||||
# sharedByUser: ID
|
||||
# ): FeedArticlesResult!
|
||||
users: UsersResult!
|
||||
validateUsername(username: String!): Boolean!
|
||||
# getFollowers(userId: ID): GetFollowersResult!
|
||||
|
|
@ -3548,6 +3637,8 @@ const schema = gql`
|
|||
hiddenHomeSection: HiddenHomeSectionResult!
|
||||
highlights(after: String, first: Int, query: String): HighlightsResult!
|
||||
folderPolicies: FolderPoliciesResult!
|
||||
posts(userId: ID!, after: String, first: Int): PostsResult!
|
||||
post(id: ID!): PostResult!
|
||||
}
|
||||
|
||||
schema {
|
||||
|
|
|
|||
88
packages/api/src/services/post.ts
Normal file
88
packages/api/src/services/post.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { DeepPartial } from 'typeorm'
|
||||
import { Post } from '../entity/post'
|
||||
import { Profile } from '../entity/profile'
|
||||
import { authTrx, getRepository } from '../repository'
|
||||
|
||||
export const findPublicPostsByUserId = async (
|
||||
userId: string,
|
||||
limit: number,
|
||||
offset: number
|
||||
) => {
|
||||
const posts = await getRepository(Post).find({
|
||||
where: {
|
||||
user: {
|
||||
id: userId,
|
||||
profile: {
|
||||
private: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
take: limit,
|
||||
skip: offset,
|
||||
})
|
||||
|
||||
return posts
|
||||
}
|
||||
|
||||
export const createPublicPost = async (
|
||||
userId: string,
|
||||
post: DeepPartial<Post>
|
||||
) => {
|
||||
return authTrx(
|
||||
async (trx) => {
|
||||
const newPost = await trx.getRepository(Post).save(post)
|
||||
|
||||
// Make user profile public when user creates a post
|
||||
await trx.getRepository(Profile).update(
|
||||
{ user: { id: userId } },
|
||||
{
|
||||
private: false,
|
||||
}
|
||||
)
|
||||
|
||||
return newPost
|
||||
},
|
||||
{
|
||||
uid: userId,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const createPosts = async (
|
||||
userId: string,
|
||||
posts: Array<DeepPartial<Post>>
|
||||
) => {
|
||||
return authTrx(async (trx) => trx.getRepository(Post).save(posts), {
|
||||
uid: userId,
|
||||
})
|
||||
}
|
||||
|
||||
export const deletePosts = async (userId: string, postIds: string[]) => {
|
||||
return authTrx(async (trx) => trx.getRepository(Post).delete(postIds), {
|
||||
uid: userId,
|
||||
})
|
||||
}
|
||||
|
||||
export const findPublicPostById = async (id: string) => {
|
||||
return getRepository(Post).findOneBy({
|
||||
id,
|
||||
user: {
|
||||
profile: {
|
||||
private: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const updatePost = async (
|
||||
userId: string,
|
||||
postId: string,
|
||||
post: Partial<Post>
|
||||
) => {
|
||||
return authTrx(async (trx) => trx.getRepository(Post).update(postId, post), {
|
||||
uid: userId,
|
||||
})
|
||||
}
|
||||
|
|
@ -1,7 +1,21 @@
|
|||
import { Profile } from '../entity/profile'
|
||||
import { User } from '../entity/user'
|
||||
import { getRepository } from '../repository'
|
||||
import { authTrx, getRepository } from '../repository'
|
||||
|
||||
export const findProfile = async (user: User): Promise<Profile | null> => {
|
||||
return getRepository(Profile).findOneBy({ user: { id: user.id } })
|
||||
}
|
||||
|
||||
export const updateProfile = async (
|
||||
userId: string,
|
||||
profile: Partial<Profile>
|
||||
) => {
|
||||
return authTrx(
|
||||
(tx) => {
|
||||
return tx.getRepository(Profile).update({ user: { id: userId } }, profile)
|
||||
},
|
||||
{
|
||||
uid: userId,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
492
packages/api/test/resolvers/post.test.ts
Normal file
492
packages/api/test/resolvers/post.test.ts
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
import { expect } from 'chai'
|
||||
import { LibraryItem } from '../../src/entity/library_item'
|
||||
import { User } from '../../src/entity/user'
|
||||
import { deleteLibraryItemById } from '../../src/services/library_item'
|
||||
import {
|
||||
createPosts,
|
||||
deletePosts,
|
||||
findPublicPostById,
|
||||
} from '../../src/services/post'
|
||||
import { findProfile, updateProfile } from '../../src/services/profile'
|
||||
import { deleteUser } from '../../src/services/user'
|
||||
import { createTestLibraryItem, createTestUser } from '../db'
|
||||
import { generateFakeUuid, graphqlRequest, loginAndGetAuthToken } from '../util'
|
||||
|
||||
describe('Post Resolvers', () => {
|
||||
let loginUser: User
|
||||
let authToken: string
|
||||
|
||||
before(async () => {
|
||||
// create test user and login
|
||||
loginUser = await createTestUser('loginUser')
|
||||
authToken = await loginAndGetAuthToken(loginUser.email)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await deleteUser(loginUser.id)
|
||||
})
|
||||
|
||||
describe('postsResolver', () => {
|
||||
const query = `
|
||||
query Posts($first: Int, $after: String, $userId: ID!) {
|
||||
posts(first: $first, after: $after, userId: $userId) {
|
||||
... on PostsSuccess {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
title
|
||||
content
|
||||
ownedByViewer
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
startCursor
|
||||
endCursor
|
||||
hasPreviousPage
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
... on PostsError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let postIds: Array<string> = []
|
||||
let libraryItem: LibraryItem
|
||||
let libraryItem1: LibraryItem
|
||||
|
||||
before(async () => {
|
||||
libraryItem = await createTestLibraryItem(loginUser.id)
|
||||
libraryItem1 = await createTestLibraryItem(loginUser.id)
|
||||
|
||||
const posts = [
|
||||
{
|
||||
title: 'Post 1',
|
||||
content: 'Content 1',
|
||||
user: loginUser,
|
||||
createdAt: new Date('2021-01-01'),
|
||||
libraryItemIds: [libraryItem.id],
|
||||
},
|
||||
{
|
||||
title: 'Post 2',
|
||||
content: 'Content 2',
|
||||
user: loginUser,
|
||||
createdAt: new Date('2021-01-02'),
|
||||
libraryItemIds: [libraryItem1.id],
|
||||
},
|
||||
]
|
||||
const newPosts = await createPosts(loginUser.id, posts)
|
||||
|
||||
postIds = newPosts.map((post) => post.id)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await deletePosts(loginUser.id, postIds)
|
||||
|
||||
await deleteLibraryItemById(libraryItem.id, loginUser.id)
|
||||
await deleteLibraryItemById(libraryItem1.id, loginUser.id)
|
||||
})
|
||||
|
||||
it('should return an error if the args are invalid', async () => {
|
||||
const response = await graphqlRequest(query, '', {
|
||||
first: 100,
|
||||
userId: loginUser.id,
|
||||
})
|
||||
|
||||
expect(response.body.data.posts.errorCodes).to.eql(['BAD_REQUEST'])
|
||||
})
|
||||
|
||||
context('when the user is authenticated', () => {
|
||||
it('should return posts', async () => {
|
||||
const response = await graphqlRequest(query, authToken, {
|
||||
first: 10,
|
||||
userId: loginUser.id,
|
||||
})
|
||||
|
||||
expect(response.body.data.posts.edges[0].node.id).to.eql(postIds[1])
|
||||
expect(response.body.data.posts.edges[1].node.id).to.eql(postIds[0])
|
||||
expect(response.body.data.posts.edges[0].node.ownedByViewer).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
context('when the user is not authenticated', () => {
|
||||
context('when user profile is public', () => {
|
||||
before(async () => {
|
||||
await updateProfile(loginUser.id, { private: false })
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await updateProfile(loginUser.id, { private: true })
|
||||
})
|
||||
|
||||
it('should return posts', async () => {
|
||||
const response = await graphqlRequest(query, '', {
|
||||
first: 10,
|
||||
userId: loginUser.id,
|
||||
})
|
||||
|
||||
expect(response.body.data.posts.edges[0].node.id).to.eql(postIds[1])
|
||||
expect(response.body.data.posts.edges[1].node.id).to.eql(postIds[0])
|
||||
expect(response.body.data.posts.edges[0].node.ownedByViewer).to.be
|
||||
.false
|
||||
})
|
||||
})
|
||||
|
||||
context('when user profile is private', () => {
|
||||
before(async () => {
|
||||
await updateProfile(loginUser.id, { private: true })
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await updateProfile(loginUser.id, { private: false })
|
||||
})
|
||||
|
||||
it('should return empty array', async () => {
|
||||
const response = await graphqlRequest(query, '', {
|
||||
first: 10,
|
||||
userId: loginUser.id,
|
||||
})
|
||||
|
||||
expect(response.body.data.posts.edges).to.be.empty
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('postResolver', () => {
|
||||
const query = `
|
||||
query Post($id: ID!) {
|
||||
post(id: $id) {
|
||||
... on PostSuccess {
|
||||
post {
|
||||
id
|
||||
title
|
||||
content
|
||||
ownedByViewer
|
||||
}
|
||||
}
|
||||
... on PostError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let postId: string
|
||||
let libraryItem: LibraryItem
|
||||
|
||||
before(async () => {
|
||||
libraryItem = await createTestLibraryItem(loginUser.id)
|
||||
|
||||
const post = {
|
||||
title: 'Post',
|
||||
content: 'Content',
|
||||
user: loginUser,
|
||||
libraryItemIds: [libraryItem.id],
|
||||
}
|
||||
const newPost = await createPosts(loginUser.id, [post])
|
||||
|
||||
postId = newPost[0].id
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await deletePosts(loginUser.id, [postId])
|
||||
await deleteLibraryItemById(libraryItem.id, loginUser.id)
|
||||
})
|
||||
|
||||
it('should return an error if the args are invalid', async () => {
|
||||
const response = await graphqlRequest(query, '', {
|
||||
id: '',
|
||||
})
|
||||
|
||||
expect(response.body.data.post.errorCodes).to.eql(['BAD_REQUEST'])
|
||||
})
|
||||
|
||||
it('should return an error if the post is not found', async () => {
|
||||
const response = await graphqlRequest(query, '', {
|
||||
id: generateFakeUuid(),
|
||||
})
|
||||
|
||||
expect(response.body.data.post.errorCodes).to.eql(['NOT_FOUND'])
|
||||
})
|
||||
|
||||
context('when the user is authenticated', () => {
|
||||
it('should return the post', async () => {
|
||||
const response = await graphqlRequest(query, authToken, {
|
||||
id: postId,
|
||||
})
|
||||
|
||||
expect(response.body.data.post.post.id).to.eql(postId)
|
||||
expect(response.body.data.post.post.ownedByViewer).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
context('when the user is not authenticated', () => {
|
||||
context('when user profile is public', () => {
|
||||
before(async () => {
|
||||
await updateProfile(loginUser.id, { private: false })
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await updateProfile(loginUser.id, { private: true })
|
||||
})
|
||||
|
||||
it('should return the post', async () => {
|
||||
const response = await graphqlRequest(query, '', {
|
||||
id: postId,
|
||||
})
|
||||
|
||||
expect(response.body.data.post.post.id).to.eql(postId)
|
||||
expect(response.body.data.post.post.ownedByViewer).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
context('when user profile is private', () => {
|
||||
before(async () => {
|
||||
await updateProfile(loginUser.id, { private: true })
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await updateProfile(loginUser.id, { private: false })
|
||||
})
|
||||
|
||||
it('should return an error', async () => {
|
||||
const response = await graphqlRequest(query, '', {
|
||||
id: postId,
|
||||
})
|
||||
|
||||
expect(response.body.data.post.errorCodes).to.eql(['NOT_FOUND'])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('createPostResolver', () => {
|
||||
const mutation = `
|
||||
mutation CreatePost($input: CreatePostInput!) {
|
||||
createPost(input: $input) {
|
||||
... on CreatePostSuccess {
|
||||
post {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
... on CreatePostError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
it('should create a post', async () => {
|
||||
const libraryItem = await createTestLibraryItem(loginUser.id)
|
||||
|
||||
const response = await graphqlRequest(mutation, authToken, {
|
||||
input: {
|
||||
title: 'Post',
|
||||
content: 'Content',
|
||||
libraryItemIds: [libraryItem.id],
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.body.data.createPost.post.title).to.eql('Post')
|
||||
expect(response.body.data.createPost.post.content).to.eql('Content')
|
||||
|
||||
const postId = response.body.data.createPost.post.id as string
|
||||
|
||||
const post = await findPublicPostById(postId)
|
||||
expect(post).to.exist
|
||||
expect(post?.title).to.eql('Post')
|
||||
|
||||
const profile = await findProfile(loginUser)
|
||||
expect(profile?.private).to.be.false
|
||||
|
||||
await deletePosts(loginUser.id, [postId])
|
||||
await deleteLibraryItemById(libraryItem.id, loginUser.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updatePostResolver', () => {
|
||||
const mutation = `
|
||||
mutation UpdatePost($input: UpdatePostInput!) {
|
||||
updatePost(input: $input) {
|
||||
... on UpdatePostSuccess {
|
||||
post {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
... on UpdatePostError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let postId: string
|
||||
let libraryItem: LibraryItem
|
||||
|
||||
before(async () => {
|
||||
libraryItem = await createTestLibraryItem(loginUser.id)
|
||||
|
||||
const post = {
|
||||
title: 'Post',
|
||||
content: 'Content',
|
||||
user: loginUser,
|
||||
libraryItemIds: [libraryItem.id],
|
||||
}
|
||||
const newPost = await createPosts(loginUser.id, [post])
|
||||
|
||||
postId = newPost[0].id
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await deletePosts(loginUser.id, [postId])
|
||||
await deleteLibraryItemById(libraryItem.id, loginUser.id)
|
||||
})
|
||||
|
||||
it('should return an error if the args are invalid', async () => {
|
||||
const response = await graphqlRequest(mutation, authToken, {
|
||||
input: {
|
||||
id: postId,
|
||||
title: null,
|
||||
content: null,
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.body.data.updatePost.errorCodes).to.eql(['BAD_REQUEST'])
|
||||
})
|
||||
|
||||
it('should return an error if the post is not found', async () => {
|
||||
const response = await graphqlRequest(mutation, authToken, {
|
||||
input: {
|
||||
id: generateFakeUuid(),
|
||||
title: 'Post',
|
||||
content: 'Content',
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.body.data.updatePost.errorCodes).to.eql(['UNAUTHORIZED'])
|
||||
})
|
||||
|
||||
it('should return an error if the user is not the owner of the post', async () => {
|
||||
const notOwner = await createTestUser('notOwner')
|
||||
const notOwnerToken = await loginAndGetAuthToken(notOwner.email)
|
||||
|
||||
const response = await graphqlRequest(mutation, notOwnerToken, {
|
||||
input: {
|
||||
id: postId,
|
||||
title: 'Post',
|
||||
content: 'Content',
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.body.data.updatePost.errorCodes).to.eql(['UNAUTHORIZED'])
|
||||
|
||||
await deleteUser(notOwner.id)
|
||||
})
|
||||
|
||||
it('should update the post', async () => {
|
||||
const response = await graphqlRequest(mutation, authToken, {
|
||||
input: {
|
||||
id: postId,
|
||||
title: 'Updated Post',
|
||||
content: 'Updated Content',
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.body.data.updatePost.post.title).to.eql('Updated Post')
|
||||
expect(response.body.data.updatePost.post.content).to.eql(
|
||||
'Updated Content'
|
||||
)
|
||||
|
||||
const post = await findPublicPostById(postId)
|
||||
expect(post?.title).to.eql('Updated Post')
|
||||
})
|
||||
})
|
||||
|
||||
describe('deletePostResolver', () => {
|
||||
const mutation = `
|
||||
mutation DeletePost($id: ID!) {
|
||||
deletePost(id: $id) {
|
||||
... on DeletePostSuccess {
|
||||
success
|
||||
}
|
||||
... on DeletePostError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let postId: string
|
||||
let libraryItem: LibraryItem
|
||||
|
||||
before(async () => {
|
||||
libraryItem = await createTestLibraryItem(loginUser.id)
|
||||
|
||||
const post = {
|
||||
title: 'Post',
|
||||
content: 'Content',
|
||||
user: loginUser,
|
||||
libraryItemIds: [libraryItem.id],
|
||||
}
|
||||
const newPost = await createPosts(loginUser.id, [post])
|
||||
|
||||
postId = newPost[0].id
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await deletePosts(loginUser.id, [postId])
|
||||
await deleteLibraryItemById(libraryItem.id, loginUser.id)
|
||||
})
|
||||
|
||||
it('should return an error if the args are invalid', async () => {
|
||||
const response = await graphqlRequest(mutation, authToken, {
|
||||
id: '',
|
||||
})
|
||||
|
||||
expect(response.body.data.deletePost.errorCodes).to.eql(['BAD_REQUEST'])
|
||||
})
|
||||
|
||||
it('should return an error if the post is not found', async () => {
|
||||
const response = await graphqlRequest(mutation, authToken, {
|
||||
id: generateFakeUuid(),
|
||||
})
|
||||
|
||||
expect(response.body.data.deletePost.errorCodes).to.eql(['UNAUTHORIZED'])
|
||||
})
|
||||
|
||||
it('should return an error if the user is not the owner of the post', async () => {
|
||||
const notOwner = await createTestUser('notOwner')
|
||||
const notOwnerToken = await loginAndGetAuthToken(notOwner.email)
|
||||
|
||||
const response = await graphqlRequest(mutation, notOwnerToken, {
|
||||
id: postId,
|
||||
})
|
||||
|
||||
expect(response.body.data.deletePost.errorCodes).to.eql(['UNAUTHORIZED'])
|
||||
|
||||
const post = await findPublicPostById(postId)
|
||||
expect(post).to.exist
|
||||
|
||||
await deleteUser(notOwner.id)
|
||||
})
|
||||
|
||||
it('should delete the post', async () => {
|
||||
const response = await graphqlRequest(mutation, authToken, {
|
||||
id: postId,
|
||||
})
|
||||
|
||||
expect(response.body.data.deletePost.success).to.be.true
|
||||
|
||||
const post = await findPublicPostById(postId)
|
||||
expect(post).to.not.exist
|
||||
})
|
||||
})
|
||||
})
|
||||
42
packages/db/migrations/0184.do.post.sql
Executable file
42
packages/db/migrations/0184.do.post.sql
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
-- Type: DO
|
||||
-- Name: post
|
||||
-- Description: Create a post table
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE omnivore.post (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v1mc(),
|
||||
user_id UUID NOT NULL REFERENCES omnivore.user(id) ON DELETE CASCADE,
|
||||
library_item_ids UUID[] NOT NULL,
|
||||
highlight_ids UUID[],
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL, -- generated from template
|
||||
thumbnail TEXT,
|
||||
thought TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX post_user_id_idx ON omnivore.post(user_id);
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.post TO omnivore_user;
|
||||
|
||||
ALTER TABLE omnivore.post ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY read_post ON omnivore.post
|
||||
FOR SELECT TO omnivore_user
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY write_post ON omnivore.post
|
||||
FOR INSERT TO omnivore_user
|
||||
WITH CHECK (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY update_post ON omnivore.post
|
||||
FOR UPDATE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
CREATE POLICY delete_post ON omnivore.post
|
||||
FOR DELETE TO omnivore_user
|
||||
USING (user_id = omnivore.get_current_user_id());
|
||||
|
||||
COMMIT;
|
||||
9
packages/db/migrations/0184.undo.post.sql
Executable file
9
packages/db/migrations/0184.undo.post.sql
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
-- Type: UNDO
|
||||
-- Name: post
|
||||
-- Description: Create a post table
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE omnivore.post;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Reference in a new issue