Merge pull request #4075 from omnivore-app/feature/posts

feat: posts api
This commit is contained in:
Hongbo Wu 2024-07-25 11:42:35 +08:00 committed by GitHub
commit ee5e66d9b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1533 additions and 31 deletions

View 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
}

View file

@ -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>;

View file

@ -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

View file

@ -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

View file

@ -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'),
}

View 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,
}
})

View file

@ -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 {

View 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,
})
}

View file

@ -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,
}
)
}

View 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
})
})
})

View 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;

View file

@ -0,0 +1,9 @@
-- Type: UNDO
-- Name: post
-- Description: Create a post table
BEGIN;
DROP TABLE omnivore.post;
COMMIT;