import { Body, Controller, Delete, ForbiddenException, Get, HttpCode, HttpStatus, type MessageEvent, NotFoundException, Param, ParseIntPipe, Patch, Post, Query, Sse, UseGuards, } from '@nestjs/common'; import { Session, type UserSession } from '@thallesp/nestjs-better-auth'; import { map, type Observable } from 'rxjs'; import { CreateCommentUseCase } from '../../application/use-cases/create-comment/create-comment.use-case.js'; import { ListCommentsUseCase } from '../../application/use-cases/list-comments/list-comments.use-case.js'; import { UpdateCommentUseCase } from '../../application/use-cases/update-comment/update-comment.use-case.js'; import { DeleteCommentUseCase } from '../../application/use-cases/delete-comment/delete-comment.use-case.js'; import { WatchIssueCommentsUseCase } from '../../application/use-cases/watch-issue-comments/watch-issue-comments.use-case.js'; import { CreateCommentCommand } from '../../application/use-cases/create-comment/create-comment.command.js'; import { ListCommentsQuery } from '../../application/use-cases/list-comments/list-comments.query.js'; import { UpdateCommentCommand } from '../../application/use-cases/update-comment/update-comment.command.js'; import { DeleteCommentCommand } from '../../application/use-cases/delete-comment/delete-comment.command.js'; import { WatchIssueCommentsQuery } from '../../application/use-cases/watch-issue-comments/watch-issue-comments.query.js'; import { CreateCommentRequestDto } from '../dto/create-comment.dto.js'; import { UpdateCommentRequestDto } from '../dto/update-comment.dto.js'; import { ListCommentsDto } from '../dto/list-comments.dto.js'; import { CommentNotFoundException } from '../../domain/exceptions/comment-not-found.exception.js'; import { CommentForbiddenException } from '../../domain/exceptions/comment-forbidden.exception.js'; import { IssueNotFoundException } from '../../../issue/domain/exceptions/issue-not-found.exception.js'; import { IssueVisibleGuard } from '../guards/issue-visible.guard.js'; @Controller() export class CommentController { constructor( private readonly createCommentUseCase: CreateCommentUseCase, private readonly listCommentsUseCase: ListCommentsUseCase, private readonly updateCommentUseCase: UpdateCommentUseCase, private readonly deleteCommentUseCase: DeleteCommentUseCase, private readonly watchIssueCommentsUseCase: WatchIssueCommentsUseCase, ) {} @Get('issues/:issueId/comments') list(@Param('issueId', ParseIntPipe) issueId: number, @Query() query: ListCommentsDto, @Session() session: UserSession) { return this.handleErrors(() => this.listCommentsUseCase.execute(new ListCommentsQuery(issueId, session.user.id, Number(query.limit), query.cursor)), ); } @Post('issues/:issueId/comments') create(@Param('issueId', ParseIntPipe) issueId: number, @Body() dto: CreateCommentRequestDto, @Session() session: UserSession) { return this.handleErrors(() => this.createCommentUseCase.execute(new CreateCommentCommand(issueId, session.user.id, dto.content))); } /** * SSE-поток комментариев одной задачи: клиент открывает его на странице * задачи и закрывает при уходе. После переподключения клиенту стоит * перезапросить список — события, пришедшие во время обрыва, не повторяются. * Доступ проверяет IssueVisibleGuard — так клиент без доступа получает 404. */ @Sse('issues/:issueId/comments/events') @UseGuards(IssueVisibleGuard) events(@Param('issueId', ParseIntPipe) issueId: number, @Session() session: UserSession): Promise> { return this.handleErrors(async () => { const events$ = await this.watchIssueCommentsUseCase.execute(new WatchIssueCommentsQuery(issueId, session.user.id)); return events$.pipe(map((event) => ({ type: event.type, data: event.comment }))); }); } @Patch('comments/:id') update(@Param('id') id: string, @Body() dto: UpdateCommentRequestDto, @Session() session: UserSession) { return this.handleErrors(() => this.updateCommentUseCase.execute(new UpdateCommentCommand(id, session.user.id, dto.content))); } @Delete('comments/:id') @HttpCode(HttpStatus.NO_CONTENT) async delete(@Param('id') id: string, @Session() session: UserSession) { await this.handleErrors(() => this.deleteCommentUseCase.execute(new DeleteCommentCommand(id, session.user.id))); } /** Транслирует доменные исключения в HTTP-ответ — это забота presentation-слоя */ private async handleErrors(fn: () => Promise): Promise { try { return await fn(); } catch (error) { if (error instanceof CommentNotFoundException || error instanceof IssueNotFoundException) { throw new NotFoundException(error.message); } if (error instanceof CommentForbiddenException) { throw new ForbiddenException(error.message); } throw error; } } }