Skip to content

Ncto 178 hub add comment likes#21

Open
Louis-rollet wants to merge 6 commits intomainfrom
NCTO-178-hub-add-comment-likes
Open

Ncto 178 hub add comment likes#21
Louis-rollet wants to merge 6 commits intomainfrom
NCTO-178-hub-add-comment-likes

Conversation

@Louis-rollet
Copy link
Copy Markdown
Collaborator

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

…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.
…to/Backend into NCTO-178-hub-add-comment-likes
…date service to include fork counts in responses
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 the OptionalJwtAuthGuard can set req.user to 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.

}

@Public()
@Public()
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

There are two consecutive @public() decorators above this route; the duplicate decorator is redundant and can be removed to avoid confusion when scanning routes.

Suggested change
@Public()

Copilot uses AI. Check for mistakes.
name: project.publishedName || project.name,
shortDesc: project.publishedShortDesc || project.shortDesc,
longDesc: project.publishedLongDesc ?? project.longDesc,
tags: project.publishedTags.length > 0 ? project.publishedTags : project.tags
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
tags: project.publishedTags.length > 0 ? project.publishedTags : project.tags
tags: project.publishedTags ?? project.tags

Copilot uses AI. Check for mistakes.
Comment on lines +667 to +673
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`);
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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`);

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +35
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";

Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +61
this.prisma.comment.count({
where: { projectId, deleted: false }
})
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +172
// 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
);
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +195
const comment = await this.prisma.comment.findUnique({
where: { id: commentId },
select: {
id: true,
authorId: true,
_count: { select: { replies: true } }
}
});
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.

-- AlterTable
ALTER TABLE "Project" ADD COLUMN "publishedAt" TIMESTAMP(3),
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +103
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
};
}

Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@alexis-belmonte
Copy link
Copy Markdown
Contributor

General comment: comment/CommentService/CommentController should be renamed to project-comment/ProjectCommentService/ProjectCommentController UNLESS this is generic enough to be separate.


const COMMENT_MAX_LINE_BREAKS = 10;

function HasMaxLineBreaks(max: number, validationOptions?: ValidationOptions) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hasMaxLineBreaks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should be in the utils folder as truly useful

}
}

private mapComment(comment: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the point of this if you already have UseGuards?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants