-
Notifications
You must be signed in to change notification settings - Fork 0
Ncto 178 hub add comment likes #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
97acc28
b954d51
beef97e
25de59f
83da2ff
a6f5211
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,3 +74,5 @@ yarn.lock | |
| # Project specific | ||
| config.json | ||
| config/webrtc.json | ||
| generated_client | ||
| swagger.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "Project" ADD COLUMN "forked_from_id" INTEGER; | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "Project" ADD CONSTRAINT "Project_forked_from_id_fkey" FOREIGN KEY ("forked_from_id") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /* | ||
| Warnings: | ||
|
|
||
| - You are about to drop the column `rating` on the `Comment` table. All the data in the column will be lost. | ||
| - Added the required column `updatedAt` to the `Project` table without a default value. This is not possible if the table is not empty. | ||
|
|
||
| */ | ||
| -- DropForeignKey | ||
| ALTER TABLE "Comment" DROP CONSTRAINT "Comment_parentId_fkey"; | ||
|
|
||
| -- DropForeignKey | ||
| ALTER TABLE "Comment" DROP CONSTRAINT "Comment_projectId_fkey"; | ||
|
|
||
| -- AlterTable | ||
| ALTER TABLE "Comment" DROP COLUMN "rating"; | ||
|
|
||
| -- AlterTable | ||
| ALTER TABLE "Project" ADD COLUMN "publishedAt" TIMESTAMP(3), | ||
| ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; | ||
|
|
||
| -- CreateTable | ||
| CREATE TABLE "Like" ( | ||
| "id" SERIAL NOT NULL, | ||
| "userId" INTEGER NOT NULL, | ||
| "projectId" INTEGER NOT NULL, | ||
| "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
|
|
||
| CONSTRAINT "Like_pkey" PRIMARY KEY ("id") | ||
| ); | ||
|
|
||
| -- CreateIndex | ||
| CREATE INDEX "Like_projectId_idx" ON "Like"("projectId"); | ||
|
|
||
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "Like_userId_projectId_key" ON "Like"("userId", "projectId"); | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "Comment" ADD CONSTRAINT "Comment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "Comment" ADD CONSTRAINT "Comment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "Like" ADD CONSTRAINT "Like_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "Like" ADD CONSTRAINT "Like_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "Comment" ADD COLUMN "deleted" BOOLEAN NOT NULL DEFAULT false; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "Project" ADD COLUMN "tags" TEXT[] DEFAULT ARRAY[]::TEXT[], | ||
| ADD COLUMN "viewCount" INTEGER NOT NULL DEFAULT 0; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| -- AlterTable | ||
| ALTER TABLE "Project" ADD COLUMN "publishedLongDesc" TEXT, | ||
| ADD COLUMN "publishedName" TEXT, | ||
| ADD COLUMN "publishedShortDesc" TEXT, | ||
| ADD COLUMN "publishedTags" TEXT[] DEFAULT ARRAY[]::TEXT[]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { ExecutionContext, Injectable } from "@nestjs/common"; | ||
| import { AuthGuard } from "@nestjs/passport"; | ||
|
|
||
| @Injectable() | ||
| export class OptionalJwtAuthGuard extends AuthGuard("jwt") { | ||
| override canActivate(context: ExecutionContext) { | ||
| return super.canActivate(context); | ||
| } | ||
|
|
||
| override handleRequest<TUser = unknown>( | ||
| _err: unknown, | ||
| user: TUser | ||
| ): TUser | null { | ||
| // If the user is not authenticated, return null instead of throwing | ||
| return user || null; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| import { | ||
| Body, | ||
| Controller, | ||
| Delete, | ||
| Get, | ||
| HttpCode, | ||
| HttpStatus, | ||
| Param, | ||
| ParseIntPipe, | ||
| Post, | ||
| Put, | ||
| Query, | ||
| Req, | ||
| UseGuards | ||
| } from "@nestjs/common"; | ||
| import { | ||
| ApiBearerAuth, | ||
| ApiBody, | ||
| ApiOperation, | ||
| ApiParam, | ||
| ApiQuery, | ||
| ApiResponse, | ||
| ApiTags | ||
| } from "@nestjs/swagger"; | ||
| import { JwtAuthGuard } from "@auth/guards/jwt-auth.guard"; | ||
| import { Public } from "@auth/decorators/public.decorator"; | ||
| import { CommentService } from "./comment.service"; | ||
| import { CreateCommentDto } from "./dto/create-comment.dto"; | ||
| import { | ||
| CommentResponseDto, | ||
| PaginatedCommentsResponseDto | ||
| } from "./dto/comment-response.dto"; | ||
| import { UserDto } from "@auth/dto/user.dto"; | ||
| import { Request } from "express"; | ||
| import { PrismaService } from "@ourPrisma/prisma.service"; | ||
|
|
||
| interface RequestWithUser extends Request { | ||
| user: UserDto; | ||
| } | ||
|
|
||
| @ApiTags("comments") | ||
| @Controller("projects/:projectId/comments") | ||
| @UseGuards(JwtAuthGuard) | ||
| @ApiBearerAuth("JWT-auth") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the point of this if you already have |
||
| export class CommentController { | ||
| constructor( | ||
| private readonly commentService: CommentService, | ||
| private readonly prisma: PrismaService | ||
| ) {} | ||
|
|
||
| @Public() | ||
| @Get() | ||
| @ApiOperation({ summary: "Get comments for a project" }) | ||
| @ApiParam({ name: "projectId", type: "number" }) | ||
| @ApiQuery({ name: "page", type: "number", required: false }) | ||
| @ApiQuery({ name: "limit", type: "number", required: false }) | ||
| @ApiQuery({ | ||
| name: "sort", | ||
| enum: ["newest", "oldest"], | ||
| required: false | ||
| }) | ||
| @ApiResponse({ | ||
| status: 200, | ||
| description: "Paginated list of comments", | ||
| type: PaginatedCommentsResponseDto | ||
| }) | ||
| async getComments( | ||
| @Param("projectId", ParseIntPipe) projectId: number, | ||
| @Query("page") page?: string, | ||
| @Query("limit") limit?: string, | ||
| @Query("sort") sort?: "newest" | "oldest" | ||
| ): Promise<PaginatedCommentsResponseDto> { | ||
| return this.commentService.getComments( | ||
| projectId, | ||
| page ? parseInt(page, 10) : 1, | ||
| limit ? parseInt(limit, 10) : 20, | ||
| sort || "newest" | ||
| ); | ||
| } | ||
|
|
||
| @Post() | ||
| @ApiOperation({ summary: "Create a comment on a project" }) | ||
| @ApiParam({ name: "projectId", type: "number" }) | ||
| @ApiBody({ type: CreateCommentDto }) | ||
| @ApiResponse({ | ||
| status: 201, | ||
| description: "Comment created", | ||
| type: CommentResponseDto | ||
| }) | ||
| @HttpCode(HttpStatus.CREATED) | ||
| async createComment( | ||
| @Param("projectId", ParseIntPipe) projectId: number, | ||
| @Body() createCommentDto: CreateCommentDto, | ||
| @Req() req: RequestWithUser | ||
| ): Promise<CommentResponseDto> { | ||
| return this.commentService.createComment( | ||
| projectId, | ||
| req.user.id, | ||
| createCommentDto.content | ||
| ); | ||
| } | ||
|
|
||
| @Post(":commentId/reply") | ||
| @ApiOperation({ summary: "Reply to a comment" }) | ||
| @ApiParam({ name: "projectId", type: "number" }) | ||
| @ApiParam({ name: "commentId", type: "number" }) | ||
| @ApiBody({ type: CreateCommentDto }) | ||
| @ApiResponse({ | ||
| status: 201, | ||
| description: "Reply created", | ||
| type: CommentResponseDto | ||
| }) | ||
| @HttpCode(HttpStatus.CREATED) | ||
| async createReply( | ||
| @Param("projectId", ParseIntPipe) projectId: number, | ||
| @Param("commentId", ParseIntPipe) commentId: number, | ||
| @Body() createCommentDto: CreateCommentDto, | ||
| @Req() req: RequestWithUser | ||
| ): Promise<CommentResponseDto> { | ||
| return this.commentService.createReply( | ||
| projectId, | ||
| commentId, | ||
| req.user.id, | ||
| createCommentDto.content | ||
| ); | ||
| } | ||
|
|
||
| @Put(":commentId") | ||
| @ApiOperation({ summary: "Edit a comment" }) | ||
| @ApiParam({ name: "projectId", type: "number" }) | ||
| @ApiParam({ name: "commentId", type: "number" }) | ||
| @ApiBody({ type: CreateCommentDto }) | ||
| @ApiResponse({ | ||
| status: 200, | ||
| description: "Comment updated", | ||
| type: CommentResponseDto | ||
| }) | ||
| async updateComment( | ||
| @Param("commentId", ParseIntPipe) commentId: number, | ||
| @Body() createCommentDto: CreateCommentDto, | ||
| @Req() req: RequestWithUser | ||
| ): Promise<CommentResponseDto> { | ||
| return this.commentService.updateComment( | ||
| commentId, | ||
| req.user.id, | ||
| createCommentDto.content | ||
| ); | ||
| } | ||
|
|
||
| @Delete(":commentId") | ||
| @ApiOperation({ summary: "Delete a comment" }) | ||
| @ApiParam({ name: "projectId", type: "number" }) | ||
| @ApiParam({ name: "commentId", type: "number" }) | ||
| @ApiResponse({ status: 204, description: "Comment deleted" }) | ||
| @HttpCode(HttpStatus.NO_CONTENT) | ||
| async deleteComment( | ||
| @Param("projectId", ParseIntPipe) projectId: number, | ||
| @Param("commentId", ParseIntPipe) commentId: number, | ||
| @Req() req: RequestWithUser | ||
| ): Promise<void> { | ||
| // 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 | ||
| ); | ||
|
Comment on lines
+161
to
+172
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { NotFoundException, BadRequestException } from "@nestjs/common"; | ||
|
|
||
| export class CommentNotFoundException extends NotFoundException { | ||
| constructor(commentId: number) { | ||
| super(`Comment with ID ${commentId} not found`); | ||
| } | ||
| } | ||
|
|
||
| export class CommentProjectNotPublishedException extends BadRequestException { | ||
| constructor(projectId: number) { | ||
| super(`Project with ID ${projectId} is not published`); | ||
| } | ||
| } | ||
|
|
||
| export class CommentNestedReplyException extends BadRequestException { | ||
| constructor() { | ||
| super("Cannot reply to a reply. Only top-level comments can receive replies."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { Module } from "@nestjs/common"; | ||
| import { CommentController } from "./comment.controller"; | ||
| import { CommentService } from "./comment.service"; | ||
| import { PrismaModule } from "@ourPrisma/prisma.module"; | ||
|
|
||
| @Module({ | ||
| imports: [PrismaModule], | ||
| controllers: [CommentController], | ||
| providers: [CommentService], | ||
| exports: [CommentService] | ||
| }) | ||
| export class CommentModule {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This migration adds
Project.updatedAtasNOT NULLwith 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.