From 2267231e36cbd9e7808a67388856fc0af084bae2 Mon Sep 17 00:00:00 2001 From: Yixuan Xu Date: Sat, 24 Apr 2021 12:05:04 +0800 Subject: [PATCH 1/2] add ancestorId migration --- .../20210424030105_comment_add_ancestor/migration.sql | 2 ++ prisma/sqlite/schema.prisma | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 prisma/sqlite/migrations/20210424030105_comment_add_ancestor/migration.sql diff --git a/prisma/sqlite/migrations/20210424030105_comment_add_ancestor/migration.sql b/prisma/sqlite/migrations/20210424030105_comment_add_ancestor/migration.sql new file mode 100644 index 00000000..d6bfdb64 --- /dev/null +++ b/prisma/sqlite/migrations/20210424030105_comment_add_ancestor/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "comments" ADD COLUMN "ancestorId" TEXT; diff --git a/prisma/sqlite/schema.prisma b/prisma/sqlite/schema.prisma index 35404389..4fb4efec 100644 --- a/prisma/sqlite/schema.prisma +++ b/prisma/sqlite/schema.prisma @@ -133,9 +133,10 @@ model Comment { approved Boolean @default(false) + ancestorId String? parentId String? parent Comment? @relation("replies", fields: [parentId], references: [id]) replies Comment[] @relation("replies") - + @@map("comments") } From c4363aa5078a89e6d161f3708923413e5fed9dda Mon Sep 17 00:00:00 2001 From: Yixuan Xu Date: Sat, 24 Apr 2021 20:12:08 +0800 Subject: [PATCH 2/2] optimize comments query --- package.json | 2 + .../comment/[commentId]/replyAsModerator.ts | 4 +- pages/api/open/comments.ts | 7 +- pages/api/project/[projectId]/comments.ts | 2 +- pages/dashboard/project/[projectId].tsx | 13 +- service/comment.service.ts | 178 +++++++++++++++++- widget/components/Comment.svelte | 2 +- widget/components/Reply.svelte | 5 +- widget/index.html | 2 +- yarn.lock | 2 +- 10 files changed, 201 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index abe79cf1..98ea1375 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@emotion/styled": "^11.0.0", "@prisma/client": "^2.19.0", "@sendgrid/mail": "^7.4.2", + "@types/lodash": "^4.14.168", "autoprefixer": "^10.2.5", "axios": "^0.21.1", "class-validator": "^0.13.1", @@ -34,6 +35,7 @@ "formidable": "^1.2.2", "framer-motion": "^3.2.1", "jsonwebtoken": "^8.5.1", + "lodash": "^4.17.21", "markdown-it": "^12.0.6", "milligram": "^1.4.1", "nanoid": "^3.1.22", diff --git a/pages/api/comment/[commentId]/replyAsModerator.ts b/pages/api/comment/[commentId]/replyAsModerator.ts index 3b3bfb20..765465cd 100644 --- a/pages/api/comment/[commentId]/replyAsModerator.ts +++ b/pages/api/comment/[commentId]/replyAsModerator.ts @@ -12,6 +12,7 @@ export default async function handler( if (req.method === 'POST') { const body = req.body as { content: string + ancestorId: string } const commentId = req.query.commentId as string @@ -19,9 +20,10 @@ export default async function handler( if (!(await authService.projectOwnerGuard(project))) { return } - const created = await commentService.addCommentAsModerator( + const created = await commentService.addCommentAsModeratorV2( commentId, body.content, + body.ancestorId, ) res.json({ data: created, diff --git a/pages/api/open/comments.ts b/pages/api/open/comments.ts index e1990350..36be6591 100644 --- a/pages/api/open/comments.ts +++ b/pages/api/open/comments.ts @@ -30,9 +30,10 @@ export default async function handler( nickname: string pageUrl?: string pageTitle?: string + ancestorId?: string } - const comment = await commentService.addComment( + const comment = await commentService.addCommentV2( body.appId, body.pageId, { @@ -41,6 +42,7 @@ export default async function handler( nickname: body.nickname, }, body.parentId, + body.ancestorId, ) res.json({ @@ -53,9 +55,8 @@ export default async function handler( pageId: string } - const comments = await commentService.getComments(query.appId, { + const comments = await commentService.getCommentsV2(query.appId, { approved: true, - parentId: null, pageSlug: query.pageId, select: { by_nickname: true, diff --git a/pages/api/project/[projectId]/comments.ts b/pages/api/project/[projectId]/comments.ts index e1f52355..c0e2c002 100644 --- a/pages/api/project/[projectId]/comments.ts +++ b/pages/api/project/[projectId]/comments.ts @@ -28,7 +28,7 @@ export default async function handler( return } - const comments = await commentService.getComments(projectId, { + const comments = await commentService.getCommentsV2(projectId, { parentId: null, page: Number(page), onlyOwn: true, diff --git a/pages/dashboard/project/[projectId].tsx b/pages/dashboard/project/[projectId].tsx index 4a8b21ac..541c3322 100644 --- a/pages/dashboard/project/[projectId].tsx +++ b/pages/dashboard/project/[projectId].tsx @@ -42,9 +42,10 @@ const deleteComment = async ({ commentId }) => { return res.data } -const replyAsModerator = async ({ parentId, content }) => { +const replyAsModerator = async ({ parentId, content, ancestorId }) => { const res = await apiClient.post(`/comment/${parentId}/replyAsModerator`, { - content + content, + ancestorId, }) return res.data.data } @@ -76,7 +77,7 @@ function CommentComponent(props: { const refetch = props.refetch const comment = props.comment - + const isRoot = props.isRoot const [showReplyForm, setShowReplyForm] = React.useState(false) const approveCommentMutation = useMutation(approveComment, { @@ -106,7 +107,11 @@ function CommentComponent(props: { }) { const form = useForm() function onSubmit({ content }) { - replyMutation.mutate({ content, parentId: props.parentId }) + replyMutation.mutate({ + content, + parentId: props.parentId, + ancestorId: comment.ancestorId, + }) } const replyMutation = useMutation(replyAsModerator, { onSuccess() { diff --git a/service/comment.service.ts b/service/comment.service.ts index ae899021..942cec18 100644 --- a/service/comment.service.ts +++ b/service/comment.service.ts @@ -5,7 +5,7 @@ import { PageService } from './page.service' import dayjs from 'dayjs' import MarkdownIt from 'markdown-it' import { HookService } from './hook.service' - +import { groupBy, get } from 'lodash' export const markdown = MarkdownIt({ linkify: true, }) @@ -15,7 +15,9 @@ markdown.disable(['image', 'link']) export class CommentService extends RequestScopeService { pageService = new PageService(this.req) hookService = new HookService(this.req) - + /** + * @deprecated + */ async getComments( projectId: string, options?: { @@ -35,6 +37,8 @@ export class CommentService extends RequestScopeService { content: true, ...options?.select, page: true, + ancestorId: true, + parentId: true, } const comments = await prisma.comment.findMany({ @@ -96,6 +100,107 @@ export class CommentService extends RequestScopeService { return allComments as any[] } + async getCommentsV2( + projectId: string, + options?: { + parentId?: string + page?: number + select?: Prisma.CommentSelect + pageSlug?: string | Prisma.StringFilter + onlyOwn?: boolean + approved?: boolean + }, + ): Promise { + const pageSize = 10 + + const select = { + id: true, + createdAt: true, + content: true, + ...options?.select, + page: true, + ancestorId: true, + parentId: true, + } + + const rootComments = await prisma.comment.findMany({ + skip: options?.page ? (options.page - 1) * pageSize : 0, + take: options?.page ? pageSize : 100, + orderBy: { + createdAt: 'desc', + }, + select, + where: { + approved: options?.approved === true ? true : options?.approved, + parentId: null, + deletedAt: { + equals: null, + }, + page: { + slug: options?.pageSlug, + projectId, + project: { + ownerId: options?.onlyOwn + ? await (await this.getSession()).uid + : undefined, + }, + }, + }, + }) + + const rootId = rootComments.map((v) => v.id) + + const replies = await prisma.comment.findMany({ + orderBy: { + createdAt: 'desc', + }, + select, + where: { + approved: options?.approved === true ? true : options?.approved, + deletedAt: { + equals: null, + }, + ancestorId: { + in: rootId, + }, + page: { + slug: options?.pageSlug, + projectId, + }, + }, + }) + + const parseResult = (v) => ({ + ...v, + parsedCreatedAt: dayjs(v.createdAt).format('YYYY-MM-DD HH:mm'), + parsedContent: markdown.render(v.content), + }) + + const parsedComment = rootComments.map((v) => parseResult(v)) + const parsedReply = replies.map((v) => parseResult(v)) + const childrenDict = groupBy(parsedReply, 'parentId') + const makeChildren = (v) => { + if (get(childrenDict, v.id)) { + return { + ...v, + replies: get(childrenDict, v.id).map((v) => makeChildren(v)), + } + } + return { + ...v, + replies: [], + } + } + + const replaiesTree = parsedReply.map((v) => makeChildren(v)) + const replaiesDict = groupBy(replaiesTree, 'parentId') + + return parsedComment.map((v) => ({ + ...v, + replies: get(replaiesDict, v.id, []), + })) + } + async getProject(commentId: string) { const res = await prisma.comment.findUnique({ where: { @@ -117,7 +222,9 @@ export class CommentService extends RequestScopeService { return res.page.project } - + /** + * @deprecated + */ async addComment( projectId: string, pageSlug: string, @@ -151,6 +258,43 @@ export class CommentService extends RequestScopeService { return created } + async addCommentV2( + projectId: string, + pageSlug: string, + body: { + content: string + email: string + nickname: string + pageUrl?: string + pageTitle?: string + }, + parentId?: string, + ancestorId?: string, + ) { + // touch page + const page = await this.pageService.upsertPage(pageSlug, projectId, { + pageTitle: body.pageTitle, + pageUrl: body.pageUrl, + }) + + const created = await prisma.comment.create({ + data: { + content: body.content, + by_email: body.email, + by_nickname: body.nickname, + pageId: page.id, + parentId, + ancestorId, + }, + }) + + this.hookService.addComment(created, projectId) + + return created + } + /** + * @deprecated + */ async addCommentAsModerator(parentId: string, content: string) { const session = await this.getSession() const parent = await prisma.comment.findUnique({ @@ -174,6 +318,34 @@ export class CommentService extends RequestScopeService { return created } + async addCommentAsModeratorV2( + parentId: string, + content: string, + ancestorId?: string, + ) { + const session = await this.getSession() + const parent = await prisma.comment.findUnique({ + where: { + id: parentId, + }, + }) + + const created = await prisma.comment.create({ + data: { + content: content, + by_email: session.user.email, + by_nickname: session.user.name, + moderatorId: session.uid, + pageId: parent.pageId, + approved: true, + parentId, + ancestorId, + }, + }) + + return created + } + async approve(commentId: string) { await prisma.comment.update({ where: { diff --git a/widget/components/Comment.svelte b/widget/components/Comment.svelte index 1438c3e4..f891e122 100644 --- a/widget/components/Comment.svelte +++ b/widget/components/Comment.svelte @@ -25,7 +25,7 @@ {#if showReplyForm} - { showReplyForm = false }} /> + { showReplyForm = false }} /> {/if} {#if comment.replies.length > 0} diff --git a/widget/components/Reply.svelte b/widget/components/Reply.svelte index 2b7d17ff..f59d3382 100644 --- a/widget/components/Reply.svelte +++ b/widget/components/Reply.svelte @@ -1,7 +1,7 @@