Files
ulitka/src/modules/comment/presentation/controllers/comment.controller.ts
T
2026-10-01 12:02:03 +03:00

102 lines
5.1 KiB
TypeScript

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<Observable<MessageEvent>> {
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<T>(fn: () => Promise<T>): Promise<T> {
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;
}
}
}