Feat/comments#160
Conversation
- Can create a comment for a post - Can reply to a comment
- Fixes to the previous commit - Updated the feed algorithm to take comments into account when ranking posts
| {#if newComment.commentId} | ||
| <Dialog.Title | ||
| >{i18n.t('social.post.comments.reply.title', { | ||
| username: getCommentFromId(newComment.commentId)?.user.username, |
There was a problem hiding this comment.
The getCommentFromId function can return null, but this is not handled in the dialog title. If the commentId is set to an invalid value (e.g., a comment that was deleted), this will cause a runtime error when trying to access .user.username on null. Consider adding a fallback or validation, such as using optional chaining: getCommentFromId(newComment.commentId)?.user?.username ?? 'Unknown User'.
| username: getCommentFromId(newComment.commentId)?.user.username, | |
| username: getCommentFromId(newComment.commentId)?.user?.username ?? 'Unknown User', |
| static async getCommentsForPost(postId: Comment['postId']): Promise<Comment[]> { | ||
| const res = await pool.query<CommentTable>( | ||
| 'SELECT id FROM comment WHERE publication_id = $1 AND comment_id IS NULL ORDER BY created_at ASC', | ||
| [postId] | ||
| ); | ||
| const comments: Comment[] = []; | ||
| for (const commentId of res.rows) { | ||
| const comment = await this.getComment(commentId.id); | ||
| if (comment) comments.push(comment); | ||
| } | ||
| return comments; |
There was a problem hiding this comment.
This implementation has an N+1 query problem. For each comment ID retrieved, a separate database query is made in getComment(). This will result in poor performance when there are many comments. Consider fetching all comment data in a single query and then building the tree structure in memory, or use a recursive CTE to fetch the entire comment tree in one query.
| static async getRepliesToComment(commentId: Comment['id']): Promise<Comment[]> { | ||
| const res = await pool.query<CommentTable>( | ||
| 'SELECT id FROM comment WHERE comment_id = $1 ORDER BY created_at ASC', | ||
| [commentId] | ||
| ); | ||
| const replies: Comment[] = []; | ||
| for (const commentId of res.rows) { | ||
| const comment = await this.getComment(commentId.id); | ||
| if (comment) replies.push(comment); | ||
| } | ||
| return replies; | ||
| } | ||
|
|
||
| static async getCommentsForPost(postId: Comment['postId']): Promise<Comment[]> { | ||
| const res = await pool.query<CommentTable>( | ||
| 'SELECT id FROM comment WHERE publication_id = $1 AND comment_id IS NULL ORDER BY created_at ASC', | ||
| [postId] | ||
| ); | ||
| const comments: Comment[] = []; | ||
| for (const commentId of res.rows) { | ||
| const comment = await this.getComment(commentId.id); | ||
| if (comment) comments.push(comment); | ||
| } | ||
| return comments; |
There was a problem hiding this comment.
This implementation has an N+1 query problem. For each reply ID retrieved, a separate database query is made in getComment(). This will result in poor performance when there are many replies. Consider fetching all replies in a single query and then recursively building the tree structure, or use a recursive CTE.
| static async getRepliesToComment(commentId: Comment['id']): Promise<Comment[]> { | |
| const res = await pool.query<CommentTable>( | |
| 'SELECT id FROM comment WHERE comment_id = $1 ORDER BY created_at ASC', | |
| [commentId] | |
| ); | |
| const replies: Comment[] = []; | |
| for (const commentId of res.rows) { | |
| const comment = await this.getComment(commentId.id); | |
| if (comment) replies.push(comment); | |
| } | |
| return replies; | |
| } | |
| static async getCommentsForPost(postId: Comment['postId']): Promise<Comment[]> { | |
| const res = await pool.query<CommentTable>( | |
| 'SELECT id FROM comment WHERE publication_id = $1 AND comment_id IS NULL ORDER BY created_at ASC', | |
| [postId] | |
| ); | |
| const comments: Comment[] = []; | |
| for (const commentId of res.rows) { | |
| const comment = await this.getComment(commentId.id); | |
| if (comment) comments.push(comment); | |
| } | |
| return comments; | |
| private static async buildCommentTree( | |
| rows: CommentTable[], | |
| rootParentId: Comment['id'] | null | |
| ): Promise<Comment[]> { | |
| const childrenByParent = new Map<Comment['id'] | null, CommentTable[]>(); | |
| for (const row of rows) { | |
| const parentId = (row.comment_id as Comment['id'] | null) ?? null; | |
| const existing = childrenByParent.get(parentId); | |
| if (existing) { | |
| existing.push(row); | |
| } else { | |
| childrenByParent.set(parentId, [row]); | |
| } | |
| } | |
| const buildForParent = async (parentId: Comment['id'] | null): Promise<Comment[]> => { | |
| const children = childrenByParent.get(parentId) ?? []; | |
| const result: Comment[] = []; | |
| for (const childRow of children) { | |
| const replies = await buildForParent(childRow.id); | |
| const associatedUser = await UserDAO.getUserById(childRow.user_id); | |
| const comment = this.convertToComment(childRow, associatedUser, replies); | |
| result.push(comment); | |
| } | |
| return result; | |
| }; | |
| return buildForParent(rootParentId); | |
| } | |
| static async getRepliesToComment(commentId: Comment['id']): Promise<Comment[]> { | |
| const res = await pool.query<CommentTable>( | |
| ` | |
| WITH RECURSIVE comment_tree AS ( | |
| SELECT * | |
| FROM comment | |
| WHERE comment_id = $1 | |
| UNION ALL | |
| SELECT c.* | |
| FROM comment c | |
| INNER JOIN comment_tree ct ON c.comment_id = ct.id | |
| ) | |
| SELECT * | |
| FROM comment_tree | |
| ORDER BY created_at ASC | |
| `, | |
| [commentId] | |
| ); | |
| return this.buildCommentTree(res.rows, commentId); | |
| } | |
| static async getCommentsForPost(postId: Comment['postId']): Promise<Comment[]> { | |
| const res = await pool.query<CommentTable>( | |
| ` | |
| WITH RECURSIVE comment_tree AS ( | |
| SELECT * | |
| FROM comment | |
| WHERE publication_id = $1 AND comment_id IS NULL | |
| UNION ALL | |
| SELECT c.* | |
| FROM comment c | |
| INNER JOIN comment_tree ct ON c.comment_id = ct.id | |
| ) | |
| SELECT * | |
| FROM comment_tree | |
| ORDER BY created_at ASC | |
| `, | |
| [postId] | |
| ); | |
| return this.buildCommentTree(res.rows, null); |
|
|
||
| newComment: async ({ locals, params, request }) => { | ||
| const schema = z.object({ | ||
| content: z.string().min(1, 'errors.social.post.comments.addComment.empty'), |
There was a problem hiding this comment.
The schema validation allows empty strings after trimming. A user could submit a comment with only whitespace characters that passes the min(1) validation but becomes empty after trim(). Consider adding .trim() to the zod schema using z.string().trim().min(1) to ensure validation happens after trimming.
| content: z.string().min(1, 'errors.social.post.comments.addComment.empty'), | |
| content: z.string().trim().min(1, 'errors.social.post.comments.addComment.empty'), |
No description provided.