diff --git a/__tests__/comments.ts b/__tests__/comments.ts index 31b4615f43..35f4d8d0f1 100644 --- a/__tests__/comments.ts +++ b/__tests__/comments.ts @@ -408,6 +408,48 @@ describe('query userComments', () => { }); }); +describe('query topComments', () => { + const QUERY = `query TopComments($postId: ID!, $first: Int) { + topComments(postId: $postId, first: $first) { + id + numUpvotes + content + } + }`; + + it('should return comments ordered by upvotes descending', async () => { + // Set different upvote counts for comments on p1 + await con.getRepository(Comment).update({ id: 'c1' }, { upvotes: 5 }); + await con.getRepository(Comment).update({ id: 'c3' }, { upvotes: 10 }); + await con.getRepository(Comment).update({ id: 'c6' }, { upvotes: 3 }); + + const res = await client.query(QUERY, { variables: { postId: 'p1' } }); + expect(res.errors).toBeFalsy(); + expect(res.data.topComments).toHaveLength(3); + // Should be ordered by upvotes descending + expect(res.data.topComments[0].id).toEqual('c3'); + expect(res.data.topComments[0].numUpvotes).toEqual(10); + expect(res.data.topComments[1].id).toEqual('c1'); + expect(res.data.topComments[1].numUpvotes).toEqual(5); + expect(res.data.topComments[2].id).toEqual('c6'); + expect(res.data.topComments[2].numUpvotes).toEqual(3); + }); + + it('should support querying by post slug', async () => { + // The slug is auto-generated from title and id: 'P1' + 'p1' -> 'p1-p1' + await con.getRepository(Comment).update({ id: 'c1' }, { upvotes: 5 }); + await con.getRepository(Comment).update({ id: 'c3' }, { upvotes: 10 }); + + const res = await client.query(QUERY, { + variables: { postId: 'p1-p1' }, + }); + expect(res.errors).toBeFalsy(); + expect(res.data.topComments).toHaveLength(3); + expect(res.data.topComments[0].id).toEqual('c3'); + expect(res.data.topComments[0].numUpvotes).toEqual(10); + }); +}); + describe('query commentFeed', () => { const QUERY = `query CommentFeed($after: String, $first: Int) { commentFeed(after: $after, first: $first) { diff --git a/src/schema/comments.ts b/src/schema/comments.ts index ad80441aa6..0fa3d8174d 100644 --- a/src/schema/comments.ts +++ b/src/schema/comments.ts @@ -433,6 +433,21 @@ export const typeDefs = /* GraphQL */ ` """ id: ID! ): CommentBalance! + + """ + Get top comments of a post ordered by upvotes + """ + topComments( + """ + Post id + """ + postId: ID! + + """ + Number of comments to return (max 20) + """ + first: Int + ): [Comment!]! } extend type Mutation { @@ -521,6 +536,11 @@ export interface GQLUserCommentsArgs extends ConnectionArguments { userId: string; } +export interface GQLTopCommentsArgs { + postId: string; + first?: number; +} + export interface MentionedUser { id: string; username?: string; @@ -986,6 +1006,34 @@ export const resolvers: IResolvers = traceResolvers< return result; }, + topComments: async ( + _, + args: GQLTopCommentsArgs, + ctx: Context, + info, + ): Promise => { + const maxLimit = 20; + const limit = Math.min(args.first ?? maxLimit, maxLimit); + + const post = await ctx.con.getRepository(Post).findOneOrFail({ + select: ['id', 'sourceId'], + where: [{ id: args.postId }, { slug: args.postId }], + }); + await ensureSourcePermissions(ctx, post.sourceId); + + return graphorm.query(ctx, info, (builder) => { + builder.queryBuilder = builder.queryBuilder + .andWhere(`${builder.alias}.postId = :postId`, { + postId: post.id, + }) + .andWhere(`${builder.alias}.parentId is null`) + .andWhere(whereVordrFilter(builder.alias, ctx.userId)) + .orderBy(`${builder.alias}.upvotes`, 'DESC') + .limit(limit); + + return builder; + }); + }, }, Mutation: { commentOnPost: async (