Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions src/app/api/comment/comment.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { sendCommentCreateNotifications } from '@/jobs/notifications'
import { sendReplyCreateNotifications } from '@/jobs/notifications/send-reply-create-notifications'
import { InitiatedEntity } from '@/types/common'
import { CreateComment, UpdateComment } from '@/types/dto/comment.dto'
import { CommentsPublicFilterType, CommentWithAttachments, CreateComment, UpdateComment } from '@/types/dto/comment.dto'
import { getArrayDifference, getArrayIntersection } from '@/utils/array'
import { CommentAddedSchema } from '@api/activity-logs/schemas/CommentAddedSchema'
import { ActivityLogger } from '@api/activity-logs/services/activity-logger.service'
Expand All @@ -12,7 +12,7 @@ import { PoliciesService } from '@api/core/services/policies.service'
import { Resource } from '@api/core/types/api'
import { UserAction } from '@api/core/types/user'
import { TasksService } from '@api/tasks/tasks.service'
import { ActivityType, Comment, CommentInitiator } from '@prisma/client'
import { ActivityType, Comment, CommentInitiator, Prisma } from '@prisma/client'
import httpStatus from 'http-status'
import { z } from 'zod'

Expand Down Expand Up @@ -266,4 +266,40 @@ export class CommentService extends BaseService {
return { ...comment, initiator }
})
}

async getAllComments(queryFilters: CommentsPublicFilterType): Promise<CommentWithAttachments[]> {
const { parentId, taskId, limit, lastIdCursor, initiatorId } = queryFilters
const where = {
parentId,
taskId,
initiatorId,
workspaceId: this.user.workspaceId,
}

const pagination: Prisma.CommentFindManyArgs = {
take: limit,
cursor: lastIdCursor ? { id: lastIdCursor } : undefined,
skip: lastIdCursor ? 1 : undefined,
}

return await this.db.comment.findMany({
where,
...pagination,
include: { attachments: true },
orderBy: { createdAt: 'desc' },
})
}
Comment on lines +282 to +288
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the comments table properly indexed here, can you please check

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created composite index with taskId, workspaceId, createdAt DESC.


async hasMoreCommentsAfterCursor(
id: string,
publicFilters: Partial<Parameters<CommentService['getAllComments']>[0]>,
): Promise<boolean> {
const newComment = await this.db.comment.findFirst({
where: { ...publicFilters, workspaceId: this.user.workspaceId },
cursor: { id },
skip: 1,
orderBy: { createdAt: 'desc' },
})
return !!newComment
}
}
45 changes: 45 additions & 0 deletions src/app/api/comment/public/comment-public.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { CommentService } from '@/app/api/comment/comment.service'
import authenticate from '@/app/api/core/utils/authenticate'
import { defaultLimit } from '@/constants/public-api'
import { getSearchParams } from '@/utils/request'
import { NextRequest, NextResponse } from 'next/server'
import { decode, encode } from 'js-base64'
import { PublicCommentSerializer } from '@/app/api/comment/public/comment-public.serializer'
import { CommentsPublicFilterType } from '@/types/dto/comment.dto'
import { IdParams } from '@/app/api/core/types/api'

export const getAllCommentsPublicForTask = async (req: NextRequest, { params }: IdParams) => {
const { id } = await params
const user = await authenticate(req)

const { parentCommentId, createdBy, limit, nextToken } = getSearchParams(req.nextUrl.searchParams, [
'parentCommentId',
'createdBy',
'limit',
'nextToken',
])

const publicFilters: CommentsPublicFilterType = {
taskId: id,
parentId: (parentCommentId === 'null' ? null : parentCommentId) || undefined,
initiatorId: createdBy || undefined,
}

const commentService = new CommentService(user)
const comments = await commentService.getAllComments({
limit: limit ? +limit : defaultLimit,
lastIdCursor: nextToken ? decode(nextToken) : undefined,
...publicFilters,
})

const lastCommentId = comments[comments.length - 1]?.id
const hasMoreComments = lastCommentId
? await commentService.hasMoreCommentsAfterCursor(lastCommentId, publicFilters)
: false
const base64NextToken = hasMoreComments ? encode(lastCommentId) : undefined

return NextResponse.json({
data: await PublicCommentSerializer.serializeMany(comments),
nextToken: base64NextToken,
})
}
29 changes: 29 additions & 0 deletions src/app/api/comment/public/comment-public.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { RFC3339DateSchema } from '@/types/common'
import { AssigneeType } from '@prisma/client'
import z from 'zod'

export const PublicAttachmentDtoSchema = z.object({
id: z.string().uuid(),
fileName: z.string(),
fileSize: z.number(),
mimeType: z.string(),
downloadUrl: z.string().url(),
uploadedBy: z.string().uuid(),
uploadedByUserType: z.nativeEnum(AssigneeType).nullable(),
uploadedDate: RFC3339DateSchema,
})
export type PublicAttachmentDto = z.infer<typeof PublicAttachmentDtoSchema>

export const PublicCommentDtoSchema = z.object({
id: z.string().uuid(),
object: z.literal('taskComment'),
taskId: z.string().uuid(),
parentCommentId: z.string().uuid().nullable(),
content: z.string(),
createdBy: z.string().uuid(),
createdByUserType: z.nativeEnum(AssigneeType).nullable(),
createdDate: RFC3339DateSchema,
updatedDate: RFC3339DateSchema,
attachments: z.array(PublicAttachmentDtoSchema).nullable(),
})
export type PublicCommentDto = z.infer<typeof PublicCommentDtoSchema>
70 changes: 70 additions & 0 deletions src/app/api/comment/public/comment-public.serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { PublicAttachmentDto, PublicCommentDto, PublicCommentDtoSchema } from '@/app/api/comment/public/comment-public.dto'
import { RFC3339DateSchema } from '@/types/common'
import { CommentWithAttachments } from '@/types/dto/comment.dto'
import { toRFC3339 } from '@/utils/dateHelper'
import { getSignedUrl } from '@/utils/signUrl'
import { Attachment, CommentInitiator } from '@prisma/client'
import { z } from 'zod'

export class PublicCommentSerializer {
static async serializeUnsafe(comment: CommentWithAttachments): Promise<PublicCommentDto> {
return {
id: comment.id,
object: 'taskComment',
parentCommentId: comment.parentId,
taskId: comment.taskId,
content: comment.content,
createdBy: comment.initiatorId,
createdByUserType: comment.initiatorType,
createdDate: RFC3339DateSchema.parse(toRFC3339(comment.createdAt)),
updatedDate: RFC3339DateSchema.parse(toRFC3339(comment.updatedAt)),
attachments: await PublicCommentSerializer.serializeAttachments({
attachments: comment.attachments,
uploadedByUserType: comment.initiatorType,
uploadedBy: comment.initiatorId,
}),
}
}

/**
*
* @param attachments array of Attachment
* @param uploadedBy id of the one who commented
* @param uploadedByUserType usertype of the one who commented
* @returns Array of PublicAttachmentDto
*/
static async serializeAttachments({
attachments,
uploadedByUserType,
uploadedBy,
}: {
attachments: Attachment[]
uploadedByUserType: CommentInitiator | null
uploadedBy: string
}): Promise<PublicAttachmentDto[]> {
const promises = attachments.map(async (attachment) => ({
id: attachment.id,
fileName: attachment.fileName,
fileSize: attachment.fileSize,
mimeType: attachment.fileType,
downloadUrl: z
.string({
message: `Invalid downloadUrl for attachment with id ${attachment.id}`,
})
.parse(await getSignedUrl(attachment.filePath)),
uploadedBy,
uploadedByUserType,
uploadedDate: RFC3339DateSchema.parse(toRFC3339(attachment.createdAt)),
}))
return await Promise.all(promises)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a chance that we will hit supabase api per sec ratelimits if there are 50+ attachments?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used supabase storage's createSignedUrls() to resolve this.

}

static async serialize(comment: CommentWithAttachments): Promise<PublicCommentDto> {
return PublicCommentDtoSchema.parse(await PublicCommentSerializer.serializeUnsafe(comment))
}

static async serializeMany(comments: CommentWithAttachments[]): Promise<PublicCommentDto[]> {
const serializedComments = await Promise.all(comments.map(async (comment) => PublicCommentSerializer.serialize(comment)))
return z.array(PublicCommentDtoSchema).parse(serializedComments)
}
}
4 changes: 4 additions & 0 deletions src/app/api/tasks/public/[id]/comments/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { getAllCommentsPublicForTask } from '@/app/api/comment/public/comment-public.controller'
import { withErrorHandler } from '@/app/api/core/utils/withErrorHandler'

export const GET = withErrorHandler(getAllCommentsPublicForTask)
11 changes: 11 additions & 0 deletions src/types/dto/comment.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { AttachmentResponseSchema } from './attachments.dto'
import { Attachment, Comment } from '@prisma/client'

export const CreateCommentSchema = z.object({
content: z.string(),
Expand Down Expand Up @@ -37,3 +38,13 @@ export const CommentResponseSchema: z.ZodType = z.lazy(() =>
)

export type CommentResponse = z.infer<typeof CommentResponseSchema>

export type CommentWithAttachments = Comment & { attachments: Attachment[] }

export type CommentsPublicFilterType = {
taskId: string
parentId?: string
initiatorId?: string
limit?: number
lastIdCursor?: string
}
Loading