import { Body, Controller, Delete, ForbiddenException, Get, HttpCode, HttpStatus, NotFoundException, Param, ParseIntPipe, Post, } from '@nestjs/common'; import { Session, type UserSession } from '@thallesp/nestjs-better-auth'; import { ListIssueAttachmentsUseCase } from '../../application/use-cases/list-issue-attachments/list-issue-attachments.use-case.js'; import { AttachFileUseCase } from '../../application/use-cases/attach-file/attach-file.use-case.js'; import { DetachFileUseCase } from '../../application/use-cases/detach-file/detach-file.use-case.js'; import { ListIssueAttachmentsQuery } from '../../application/use-cases/list-issue-attachments/list-issue-attachments.query.js'; import { AttachFileCommand } from '../../application/use-cases/attach-file/attach-file.command.js'; import { DetachFileCommand } from '../../application/use-cases/detach-file/detach-file.command.js'; import { AttachFileRequestDto } from '../dto/attach-file.dto.js'; import { IssueAttachmentNotFoundException } from '../../domain/exceptions/issue-attachment-not-found.exception.js'; import { IssueNotFoundException } from '../../../issue/domain/exceptions/issue-not-found.exception.js'; import { FileNotFoundException } from '../../../file/domain/exceptions/file-not-found.exception.js'; import { FileForbiddenException } from '../../../file/domain/exceptions/file-forbidden.exception.js'; import { IssueEditForbiddenException } from '../../../issue/domain/exceptions/issue-edit-forbidden.exception.js'; /** * Вложения задачи. Сам файл загружается отдельно (POST /files), * здесь — только привязка к задаче. Скачивание — GET /files/:id/download. */ @Controller('issues/:issueId/attachments') export class IssueAttachmentController { constructor( private readonly listIssueAttachmentsUseCase: ListIssueAttachmentsUseCase, private readonly attachFileUseCase: AttachFileUseCase, private readonly detachFileUseCase: DetachFileUseCase, ) {} @Get() list(@Param('issueId', ParseIntPipe) issueId: number, @Session() session: UserSession) { return this.handleErrors(() => this.listIssueAttachmentsUseCase.execute(new ListIssueAttachmentsQuery(issueId, session.user.id)), ); } @Post() attach( @Param('issueId', ParseIntPipe) issueId: number, @Body() dto: AttachFileRequestDto, @Session() session: UserSession, ) { return this.handleErrors(() => this.attachFileUseCase.execute(new AttachFileCommand(issueId, dto.fileId, session.user.id)), ); } @Delete(':fileId') @HttpCode(HttpStatus.NO_CONTENT) async detach( @Param('issueId', ParseIntPipe) issueId: number, @Param('fileId') fileId: string, @Session() session: UserSession, ) { await this.handleErrors(() => this.detachFileUseCase.execute(new DetachFileCommand(issueId, fileId, session.user.id)), ); } /** Транслирует доменные исключения в HTTP-ответ — это забота presentation-слоя */ private async handleErrors(fn: () => Promise): Promise { try { return await fn(); } catch (error) { if ( error instanceof IssueNotFoundException || error instanceof FileNotFoundException || error instanceof IssueAttachmentNotFoundException ) { throw new NotFoundException(error.message); } if (error instanceof FileForbiddenException || error instanceof IssueEditForbiddenException) { throw new ForbiddenException(error.message); } throw error; } } }