Add issue attachments
- IssueAttachment link table (issue <-> file) + migration - GET/POST /issues/:issueId/attachments, DELETE .../attachments/:fileId - Only own uploaded files can be attached; attach is idempotent - Registers issues as a file owner: attached files stay visible to issue participants and are not collected by GC - IssueDetail.attachments GraphQL field via @ResolveField, issue module untouched Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f32c6e348d
commit
423afa658d
28 files changed
+828
-16
No files matched your search
@@ -0,0 +1,87 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* Вложения задачи. Сам файл загружается отдельно (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<T>(fn: () => Promise<T>): Promise<T> {
|
||||
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) {
|
||||
throw new ForbiddenException(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user