150 lines
6.2 KiB
TypeScript
150 lines
6.2 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
HttpCode,
|
|
HttpStatus,
|
|
type MessageEvent,
|
|
NotFoundException,
|
|
Param,
|
|
ParseIntPipe,
|
|
Patch,
|
|
Post,
|
|
Query,
|
|
Sse,
|
|
} from '@nestjs/common';
|
|
import { Session, type UserSession } from '@thallesp/nestjs-better-auth';
|
|
import { filter, map, merge, type Observable } from 'rxjs';
|
|
import { IssueDto } from '../../application/dto/issue.dto.js';
|
|
import { CreateIssueUseCase } from '../../application/use-cases/create-issue/create-issue.use-case.js';
|
|
import { GetIssueUseCase } from '../../application/use-cases/get-issue/get-issue.use-case.js';
|
|
import { ListIssuesUseCase } from '../../application/use-cases/list-issues/list-issues.use-case.js';
|
|
import { UpdateIssueUseCase } from '../../application/use-cases/update-issue/update-issue.use-case.js';
|
|
import { DeleteIssueUseCase } from '../../application/use-cases/delete-issue/delete-issue.use-case.js';
|
|
import { CreateIssueCommand } from '../../application/use-cases/create-issue/create-issue.command.js';
|
|
import { GetIssueQuery } from '../../application/use-cases/get-issue/get-issue.query.js';
|
|
import { ListIssuesQuery } from '../../application/use-cases/list-issues/list-issues.query.js';
|
|
import { UpdateIssueCommand } from '../../application/use-cases/update-issue/update-issue.command.js';
|
|
import { DeleteIssueCommand } from '../../application/use-cases/delete-issue/delete-issue.command.js';
|
|
import { CreateIssueRequestDto } from '../dto/create-issue.dto.js';
|
|
import { UpdateIssueRequestDto } from '../dto/update-issue.dto.js';
|
|
import { IssueNotFoundException } from '../../domain/exceptions/issue-not-found.exception.js';
|
|
import { IssueProjectNotFoundException } from '../../domain/exceptions/issue-project-not-found.exception.js';
|
|
import { FilterIssueDto } from '../dto/filter-issue.dto.js';
|
|
import { GetIssueByCkeyUseCase } from '../../application/use-cases/get-issue-by-ckey/get-issue-by-ckey.use-case.js';
|
|
import { GetIssueByCkeyQuery } from '../../application/use-cases/get-issue-by-ckey/get-issue-by-ckey.query.js';
|
|
import { IssueEventsService } from '../../application/services/issue-events.service.js';
|
|
import { GetIssueStatsUseCase } from '../../application/use-cases/get-issue-stats/get-issue-stats.use-case.js';
|
|
|
|
@Controller('issues')
|
|
export class IssueController {
|
|
constructor(
|
|
private readonly createIssueUseCase: CreateIssueUseCase,
|
|
private readonly getIssueUseCase: GetIssueUseCase,
|
|
private readonly getIssueByCkeyUseCase: GetIssueByCkeyUseCase,
|
|
private readonly listIssuesUseCase: ListIssuesUseCase,
|
|
private readonly updateIssueUseCase: UpdateIssueUseCase,
|
|
private readonly deleteIssueUseCase: DeleteIssueUseCase,
|
|
private readonly getIssueStatsUseCase: GetIssueStatsUseCase,
|
|
private readonly issueEventsService: IssueEventsService,
|
|
) {}
|
|
|
|
@Post()
|
|
create(@Body() dto: CreateIssueRequestDto, @Session() session: UserSession) {
|
|
return this.handleErrors(() =>
|
|
this.createIssueUseCase.execute(
|
|
new CreateIssueCommand(
|
|
dto.key,
|
|
dto.title,
|
|
session.user.id,
|
|
dto.projectId,
|
|
dto.content,
|
|
dto.priority,
|
|
dto.assignee,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@Get('list')
|
|
list(@Query() query: FilterIssueDto, @Session() session: UserSession) {
|
|
return this.listIssuesUseCase.execute(
|
|
new ListIssuesQuery(Number(query.limit), Number(query.page), session.user.id, query.projectId),
|
|
);
|
|
}
|
|
|
|
@Get('stats')
|
|
stats(@Session() session: UserSession) {
|
|
return this.getIssueStatsUseCase.execute(session.user.id);
|
|
}
|
|
|
|
/**
|
|
* SSE-поток для зарегистрированных пользователей (доступ уже проверен
|
|
* глобальным AuthGuard): уведомляет о каждой созданной, обновлённой
|
|
* и удалённой задаче, видимой текущему пользователю (reporter/assignee —
|
|
* то же правило видимости, что и в IssuePrismaRepository.ownedByUser).
|
|
*/
|
|
@Sse('events')
|
|
issueCreatedEvents(@Session() session: UserSession): Observable<MessageEvent> {
|
|
const isVisibleToUser = (issue: IssueDto) =>
|
|
issue.reporterId === session.user.id || issue.assigneeId === session.user.id;
|
|
|
|
return merge(
|
|
this.issueEventsService.onIssueCreated().pipe(
|
|
filter(isVisibleToUser),
|
|
map((issue) => ({ type: 'issue.created', data: issue })),
|
|
),
|
|
this.issueEventsService.onIssueUpdated().pipe(
|
|
filter(isVisibleToUser),
|
|
map((issue) => ({ type: 'issue.updated', data: issue })),
|
|
),
|
|
this.issueEventsService.onIssueDeleted().pipe(
|
|
filter(isVisibleToUser),
|
|
map((issue) => ({ type: 'issue.deleted', data: issue })),
|
|
),
|
|
);
|
|
}
|
|
|
|
@Get(':id')
|
|
async getById(@Param('id', ParseIntPipe) id: number, @Session() session: UserSession) {
|
|
return this.handleErrors(() => this.getIssueUseCase.execute(new GetIssueQuery(id, session.user.id)));
|
|
}
|
|
|
|
@Get('by-key/:key')
|
|
async getByCKey(@Param('key') ckey: string, @Session() session: UserSession) {
|
|
return this.handleErrors(() => this.getIssueByCkeyUseCase.execute(new GetIssueByCkeyQuery(ckey, session.user.id)));
|
|
}
|
|
|
|
@Patch(':id')
|
|
async update(
|
|
@Param('id', ParseIntPipe) id: number,
|
|
@Body() dto: UpdateIssueRequestDto,
|
|
@Session() session: UserSession,
|
|
) {
|
|
return this.handleErrors(() =>
|
|
this.updateIssueUseCase.execute(
|
|
new UpdateIssueCommand(id, session.user.id, dto.title, dto.content, dto.status, dto.priority, dto.assignee),
|
|
),
|
|
);
|
|
}
|
|
|
|
@Delete(':id')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
async delete(@Param('id', ParseIntPipe) id: number, @Session() session: UserSession) {
|
|
await this.handleErrors(() => this.deleteIssueUseCase.execute(new DeleteIssueCommand(id, 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 IssueProjectNotFoundException) {
|
|
throw new NotFoundException(error.message);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|