diff --git a/packages/api/src/entity/post.ts b/packages/api/src/entity/post.ts index d689d9202..3e8bf997e 100644 --- a/packages/api/src/entity/post.ts +++ b/packages/api/src/entity/post.ts @@ -20,10 +20,10 @@ export class Post { user!: User @Column('uuid', { array: true, nullable: true }) - libraryItemIds?: string[] + libraryItemIds?: string[] | null @Column('uuid', { array: true, nullable: true }) - highlightIds?: string[] + highlightIds?: string[] | null @Column('text') title!: string @@ -32,10 +32,10 @@ export class Post { content!: string @Column('text', { nullable: true }) - thumbnail?: string + thumbnail?: string | null @Column('text', { nullable: true }) - thought?: string + thought?: string | null @Column('timestamptz') createdAt!: Date diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 8bb629929..c2a8d83a3 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -151,7 +151,12 @@ import { webhookResolver, webhooksResolver, } from './index' -import { createPostResolver, postResolver, postsResolver } from './posts' +import { + createPostResolver, + postResolver, + postsResolver, + updatePostResolver, +} from './posts' import { markEmailAsItemResolver, recentEmailsResolver, @@ -319,6 +324,7 @@ export const functionResolvers = { updateFolderPolicy: updateFolderPolicyResolver, deleteFolderPolicy: deleteFolderPolicyResolver, createPost: createPostResolver, + updatePost: updatePostResolver, }, Query: { me: getMeUserResolver, @@ -906,4 +912,5 @@ export const functionResolvers = { ...resultResolveTypeResolver('Posts'), ...resultResolveTypeResolver('Post'), ...resultResolveTypeResolver('CreatePost'), + ...resultResolveTypeResolver('UpdatePost'), } diff --git a/packages/api/src/resolvers/posts/index.ts b/packages/api/src/resolvers/posts/index.ts index 2bde99791..e76ed3dd4 100644 --- a/packages/api/src/resolvers/posts/index.ts +++ b/packages/api/src/resolvers/posts/index.ts @@ -4,6 +4,7 @@ import { CreatePostErrorCode, CreatePostSuccess, MutationCreatePostArgs, + MutationUpdatePostArgs, PostEdge, PostErrorCode, PostResult, @@ -12,12 +13,15 @@ import { QueryPostArgs, QueryPostsArgs, ResolverFn, + UpdatePostError, + UpdatePostErrorCode, + UpdatePostSuccess, } from '../../generated/graphql' import { - createPosts, createPublicPost, findPublicPostById, findPublicPostsByUserId, + updatePost, } from '../../services/post' import { Merge } from '../../util' import { authorized } from '../../utils/gql-utils' @@ -135,3 +139,57 @@ export const createPostResolver = authorized< post, } }) + +export const updatePostResolver = authorized< + Merge, + UpdatePostError, + MutationUpdatePostArgs +>(async (_, { input }, { uid, log }) => { + const { + id, + title, + content, + highlightIds, + libraryItemIds, + thought, + thumbnail, + } = input + + if (!id || title === null || content === 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, + } +}) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 07035d315..c36cb13e5 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -3378,8 +3378,8 @@ const schema = gql` input UpdatePostInput { id: ID! - title: String - content: String + title: String @sanitize(minLength: 1, maxLength: 255) + content: String @sanitize(minLength: 1) thumbnail: String libraryItemIds: [ID!] highlightIds: [ID!] diff --git a/packages/api/src/services/post.ts b/packages/api/src/services/post.ts index f67b0cd5c..f51b95f27 100644 --- a/packages/api/src/services/post.ts +++ b/packages/api/src/services/post.ts @@ -81,3 +81,13 @@ export const findPublicPostById = async (id: string) => { }, }) } + +export const updatePost = async ( + userId: string, + postId: string, + post: Partial +) => { + return authTrx(async (trx) => trx.getRepository(Post).update(postId, post), { + uid: userId, + }) +} diff --git a/packages/api/test/resolvers/post.test.ts b/packages/api/test/resolvers/post.test.ts index bc17c094f..0c28a0621 100644 --- a/packages/api/test/resolvers/post.test.ts +++ b/packages/api/test/resolvers/post.test.ts @@ -288,4 +288,99 @@ describe('Post Resolvers', () => { await deletePosts(loginUser.id, [postId]) }) }) + + describe('updatePostResolver', () => { + const mutation = ` + mutation UpdatePost($input: UpdatePostInput!) { + updatePost(input: $input) { + ... on UpdatePostSuccess { + post { + id + title + content + } + } + ... on UpdatePostError { + errorCodes + } + } + } + ` + + let postId: string + + before(async () => { + const post = { + title: 'Post', + content: 'Content', + user: loginUser, + } + const newPost = await createPosts(loginUser.id, [post]) + + postId = newPost[0].id + }) + + after(async () => { + await deletePosts(loginUser.id, [postId]) + }) + + 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') + }) + }) })