Conversation
…uding database schema changes and API updates
…handling - Add CommentModule, CommentService, and CommentController for managing comments. - Create custom exceptions for comment-related errors. - Implement methods for retrieving, creating, updating, and deleting comments and replies. - Add DTOs for comment responses and creation. - Integrate comment functionality with project entities and responses. - Update project service to include comment counts and like functionality. - Enhance Swagger documentation for comments and likes endpoints.
…rresponding DTOs and database migrations
…to/Backend into NCTO-178-hub-add-comment-likes
…date service to include fork counts in responses
There was a problem hiding this comment.
Pull request overview
This PR adds “hub” social features around published games (comments, likes, views, forks) and introduces a published-project snapshot (name/description/tags at publish time) so hub metadata can remain stable even if the draft project changes.
Changes:
- Add Comment module (CRUD + replies + soft-delete behavior) and wire it into the app and Swagger generation.
- Add project social capabilities: like (incl. optional auth), view counter, forking, and comment/fork counts on release metadata.
- Extend Prisma schema + migrations for tags, published snapshots, views, forks, and Like model; adjust DTOs accordingly.
Reviewed changes
Copilot reviewed 27 out of 29 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Adds @comment/* path alias for the new comment route package. |
| swagger.json | Removes committed Swagger output (now expected to be generated). |
| src/swagger.app.module.ts | Registers CommentController/CommentService for Swagger JSON generation. |
| src/routes/project/project.service.ts | Adds tag normalization, published snapshot logic, release update, views, likes, forks, and counts. |
| src/routes/project/project.service.spec.ts | Updates mocked Project shape to include new columns. |
| src/routes/project/project.controller.ts | Makes release endpoints public; adds fork/like/view/update-release endpoints. |
| src/routes/project/entities/project.entity.ts | Documents updatedAt and publishedAt in the Swagger entity. |
| src/routes/project/dto/view-response.dto.ts | Adds DTO for view counter response. |
| src/routes/project/dto/update-project.dto.ts | Adds optional tags validation and Swagger metadata. |
| src/routes/project/dto/project-response.dto.ts | Extends project response DTO with tags, publish/view metadata, and counts. |
| src/routes/project/dto/like-response.dto.ts | Adds DTO for like toggle/status responses. |
| src/routes/project/dto/create-project.dto.ts | Adds optional tags validation and Swagger metadata. |
| src/routes/comment/entities/comment.entity.ts | Adds Swagger entity for comments. |
| src/routes/comment/dto/create-comment.dto.ts | Adds validation/normalization for comment content (length + line breaks). |
| src/routes/comment/dto/comment-response.dto.ts | Adds comment response DTOs (author, replies, pagination). |
| src/routes/comment/comment.service.ts | Implements comment retrieval/creation/replies/edit/delete + soft-delete mapping. |
| src/routes/comment/comment.module.ts | Declares CommentModule and exports CommentService. |
| src/routes/comment/comment.error.ts | Adds typed exceptions for comment flows. |
| src/routes/comment/comment.controller.ts | Adds comment API endpoints under projects/:projectId/comments. |
| src/auth/guards/optional-jwt-auth.guard.ts | Adds an auth guard that allows anonymous requests while attaching a user when present. |
| src/app.module.ts | Registers CommentModule in the main app. |
| prisma/models/user.prisma | Adds User ↔ Like relation. |
| prisma/models/project.prisma | Adds tags, published snapshot fields, forks, viewCount, soft-delete for comments, and Like model. |
| prisma/migrations/20260405122732_add_published_project_snapshot/migration.sql | Adds published snapshot columns. |
| prisma/migrations/20260405115450_add_project_views_tags/migration.sql | Adds tags + viewCount columns. |
| prisma/migrations/20260405104557_add_comment_soft_delete/migration.sql | Adds Comment.deleted soft-delete flag. |
| prisma/migrations/20260405091118_add_likes_comments_publish_date/migration.sql | Adds Like table + publishedAt/updatedAt changes and FK updates. |
| prisma/migrations/20260405081129_add_forked_from/migration.sql | Adds forked-from foreign key. |
| .gitignore | Ignores generated_client and swagger.json artifacts. |
Comments suppressed due to low confidence (1)
src/routes/project/project.controller.ts:66
- RequestWithUser declares
user: UserDto, but theOptionalJwtAuthGuardcan setreq.userto null/undefined for anonymous requests. This type should be updated (e.g.,user?: UserDto | null) to match runtime and avoid unsafe assumptions in handlers.
interface RequestWithUser extends Request {
user: UserDto;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| name: project.publishedName || project.name, | ||
| shortDesc: project.publishedShortDesc || project.shortDesc, | ||
| longDesc: project.publishedLongDesc ?? project.longDesc, | ||
| tags: project.publishedTags.length > 0 ? project.publishedTags : project.tags |
There was a problem hiding this comment.
applyPublishedSnapshot treats an empty publishedTags array as “no snapshot” and falls back to project.tags. That makes it impossible for a published snapshot to intentionally have zero tags (and can leak draft tag edits into the hub). Use a null/undefined check (or publishedAt presence) instead of length > 0.
| tags: project.publishedTags.length > 0 ? project.publishedTags : project.tags | |
| tags: project.publishedTags ?? project.tags |
| const project = await this.prisma.project.findUnique({ | ||
| where: { id: projectId }, | ||
| select: { id: true, likes: true } | ||
| }); | ||
|
|
||
| if (!project) { | ||
| throw new NotFoundException(`Project with ID ${projectId} not found`); |
There was a problem hiding this comment.
likeProject only checks that the project exists; it doesn’t enforce that the project is published. Since the endpoint is under /releases/:id/like, this allows liking/unliking draft/private projects if the ID is known. Consider querying with where: { id: projectId, status: "COMPLETED" } (and similarly for unlikeProject/getLikeStatus) to match the route semantics.
| const project = await this.prisma.project.findUnique({ | |
| where: { id: projectId }, | |
| select: { id: true, likes: true } | |
| }); | |
| if (!project) { | |
| throw new NotFoundException(`Project with ID ${projectId} not found`); | |
| const project = await this.prisma.project.findFirst({ | |
| where: { | |
| id: projectId, | |
| status: "COMPLETED" | |
| }, | |
| select: { id: true, likes: true } | |
| }); | |
| if (!project) { | |
| throw new NotFoundException(`Published project with ID ${projectId} not found`); |
| async getComments( | ||
| projectId: number, | ||
| page: number = 1, | ||
| limit: number = 20, | ||
| sort: "newest" | "oldest" = "newest" | ||
| ): Promise<PaginatedCommentsResponseDto> { | ||
| const skip = (page - 1) * limit; | ||
| const orderBy = sort === "newest" ? "desc" : "asc"; | ||
|
|
There was a problem hiding this comment.
getComments calculates skip = (page - 1) * limit but neither page nor limit are validated. Passing page=0/negative values or a huge limit can cause Prisma errors or expensive queries. Add validation/clamping (e.g., page >= 1, limit within a reasonable max) before computing skip/take.
| this.prisma.comment.count({ | ||
| where: { projectId, deleted: false } | ||
| }) |
There was a problem hiding this comment.
The total count query uses where: { projectId, deleted: false }, but the list query includes soft-deleted top-level comments that have replies. This can make pagination inconsistent (items shown but not counted). Count with the same filter logic as findMany (or adjust the list query) so total/page/limit are coherent.
| // Check if user is the project creator for extended delete permissions | ||
| const project = await this.prisma.project.findUnique({ | ||
| where: { id: projectId }, | ||
| select: { userId: true } | ||
| }); | ||
| const isProjectCreator = project?.userId === req.user.id; | ||
|
|
||
| return this.commentService.deleteComment( | ||
| commentId, | ||
| req.user.id, | ||
| isProjectCreator | ||
| ); |
There was a problem hiding this comment.
This controller determines isProjectCreator from the projectId route param, but then deletes by commentId without verifying that the comment belongs to that project. A creator of project A could delete a comment on project B by calling DELETE /projects/A/comments/{commentIdOfB}. Pass projectId through to the service and enforce comment.projectId === projectId before allowing creator-based deletion.
| const comment = await this.prisma.comment.findUnique({ | ||
| where: { id: commentId }, | ||
| select: { | ||
| id: true, | ||
| authorId: true, | ||
| _count: { select: { replies: true } } | ||
| } | ||
| }); |
There was a problem hiding this comment.
deleteComment authorizes using isProjectCreator but does not validate the comment’s projectId. Combined with the route/controller logic, this enables cross-project deletion. Include projectId in the lookup and compare against the route param (or accept projectId as an argument) before applying creator privileges.
|
|
||
| -- AlterTable | ||
| ALTER TABLE "Project" ADD COLUMN "publishedAt" TIMESTAMP(3), | ||
| ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; |
There was a problem hiding this comment.
This migration adds Project.updatedAt as NOT NULL with no default/backfill. On a non-empty table, the ALTER TABLE will fail (as noted in the warning). Provide a default (e.g. DEFAULT now()) and/or backfill existing rows before adding the NOT NULL constraint.
| ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; | |
| ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; |
| private normalizeTags(tags?: string[]): string[] { | ||
| if (!tags) { | ||
| return []; | ||
| } | ||
|
|
||
| const normalized = tags | ||
| .map((tag) => tag.trim()) | ||
| .filter((tag) => tag.length > 0) | ||
| .slice(0, 12); | ||
|
|
||
| return normalized.filter( | ||
| (tag, index, array) => | ||
| array.findIndex( | ||
| (candidate) => candidate.toLocaleLowerCase() === tag.toLocaleLowerCase() | ||
| ) === index | ||
| ); | ||
| } | ||
|
|
||
| private withCommentCount(project: ProjectWithCounts): ReleaseProject { | ||
| const { _count, ...rest } = project; | ||
| return { | ||
| ...rest, | ||
| commentCount: _count.comments, | ||
| forkCount: _count.forks | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
New behaviors were added to ProjectService (tag normalization, published snapshot application, view registration, like toggling, forking) but the existing unit test suite in this file doesn’t cover these paths. Adding tests for these methods (especially like toggle + view increment + snapshot behavior) would help prevent regressions.
|
General comment: |
|
|
||
| const COMMENT_MAX_LINE_BREAKS = 10; | ||
|
|
||
| function HasMaxLineBreaks(max: number, validationOptions?: ValidationOptions) { |
There was a problem hiding this comment.
Should be in the utils folder as truly useful
| } | ||
| } | ||
|
|
||
| private mapComment(comment: { |
There was a problem hiding this comment.
AI is bad here. comment should have a type here, not this monstrosity
| } from "class-validator"; | ||
| import { ApiProperty } from "@nestjs/swagger"; | ||
|
|
||
| const COMMENT_MAX_LINE_BREAKS = 10; |
There was a problem hiding this comment.
Not sure why you'd want a constant if you're not using it in the controller or service
| @ApiTags("comments") | ||
| @Controller("projects/:projectId/comments") | ||
| @UseGuards(JwtAuthGuard) | ||
| @ApiBearerAuth("JWT-auth") |
There was a problem hiding this comment.
What is the point of this if you already have UseGuards?
Jira ticket
https://naucto.atlassian.net/browse/NCTO-179
https://naucto.atlassian.net/browse/NCTO-178
What does your MR do ?
Adds comments, like, fork abiltiy on games and also hub games availablitiy to people without accounts
How to test it
create a game, publish, plan and go to the hub, fork the game, comment, like
Screenshot
Notes