import { Body, Controller, Get, HttpCode, Param, Patch, Post, Query, } from '@nestjs/common'; import { ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiParam, ApiTags, } from '@nestjs/swagger'; import { User } from '@task-stream/api/identity/domain'; import { ApiOptionalPireonAuth, ApiPireonAuth, CurrentUser, } from '@task-stream/api/identity/presenters'; import { ApiCommandErrors, ApiReadErrors, } from '@task-stream/api/shared/presenters'; import { IssuesService, ProjectsService, ReleasesService, } from '@task-stream/api/tracker/application'; import { CreateReleaseDto, PageQueryDto, PublishReleaseDto, UpcomingReleasesQueryDto, UpdateReleaseDto, } from './dto/requests.dto'; import { CommandResultDto, IssuePageDto, ReleaseDto, ReleaseBurnupDto, ReleaseProgressDto, } from './dto/responses.dto'; import { viewerOf } from '../common/viewer'; import { Refs } from './refs'; const PROJECT = ApiParam({ name: 'project', description: 'Project id or key', example: 'WEB', }); /** REST adapter of the release use cases: versions that group tickets. */ @ApiTags('Releases') @ApiPireonAuth() @Controller() export class ReleasesController { constructor( private readonly releases: ReleasesService, private readonly projects: ProjectsService, private readonly issues: IssuesService, private readonly refs: Refs, ) {} @Get('me/releases') @ApiOperation({ summary: 'Planned releases of all my projects, soonest target date first', }) @ApiOkResponse({ type: [ReleaseDto] }) @ApiReadErrors() upcoming(@CurrentUser() me: User, @Query() query: UpcomingReleasesQueryDto) { return this.releases.upcoming(me.id, query.limit); } @Get('projects/:project/releases') @ApiOptionalPireonAuth() @PROJECT @ApiOperation({ summary: 'Releases of a project' }) @ApiOkResponse({ type: [ReleaseDto] }) @ApiReadErrors() async list( @CurrentUser() me: User | undefined, @Param('project') project: string, ) { return this.projects.releases( viewerOf(me), await this.refs.projectId(viewerOf(me), project), ); } @Post('projects/:project/releases') @PROJECT @ApiOperation({ summary: 'Plan a release (owner/admin)' }) @ApiCreatedResponse({ type: CommandResultDto }) @ApiCommandErrors() async create( @CurrentUser() me: User, @Param('project') project: string, @Body() body: CreateReleaseDto, ) { return this.releases.create(me.id, { ...body, projectId: await this.refs.projectId(me.id, project), }); } @Get('releases/:releaseId') @ApiOptionalPireonAuth() @ApiOperation({ summary: 'A release' }) @ApiOkResponse({ type: ReleaseDto }) @ApiReadErrors() findOne( @CurrentUser() me: User | undefined, @Param('releaseId') releaseId: string, ) { return this.releases.findOne(viewerOf(me), releaseId); } @Patch('releases/:releaseId') @ApiOperation({ summary: 'Rename, describe or move the target date' }) @ApiOkResponse({ type: CommandResultDto }) @ApiCommandErrors() update( @CurrentUser() me: User, @Param('releaseId') releaseId: string, @Body() body: UpdateReleaseDto, ) { return this.releases.update(me.id, releaseId, body); } @Get('releases/:releaseId/progress') @ApiOptionalPireonAuth() @ApiOperation({ summary: 'Finished tickets out of the total' }) @ApiOkResponse({ type: ReleaseProgressDto }) @ApiReadErrors() progress( @CurrentUser() me: User | undefined, @Param('releaseId') releaseId: string, ) { return this.releases.progress(viewerOf(me), releaseId); } @Get('releases/:releaseId/burnup') @ApiOptionalPireonAuth() @ApiOperation({ summary: 'Burnup: tickets in the release and finished ones, day by day', }) @ApiOkResponse({ type: ReleaseBurnupDto }) @ApiReadErrors() burnup( @CurrentUser() me: User | undefined, @Param('releaseId') releaseId: string, ) { return this.releases.burnup(viewerOf(me), releaseId); } @Get('releases/:releaseId/issues') @ApiOptionalPireonAuth() @ApiOperation({ summary: 'Tickets included in the release' }) @ApiOkResponse({ type: IssuePageDto }) @ApiReadErrors() async issuesOf( @CurrentUser() me: User | undefined, @Param('releaseId') releaseId: string, @Query() page: PageQueryDto, ) { const release = await this.releases.findOne(viewerOf(me), releaseId); const found = await this.issues.search(viewerOf(me), { projectId: release.projectId, releaseId: release.id, limit: page.limit, offset: page.offset, }); return { ...found, items: await this.refs.issueDtos(found.items) }; } @Post('releases/:releaseId/publish') @HttpCode(200) @ApiOperation({ summary: 'Ship the release', description: 'Fails (422) while it has unfinished tickets unless `force` is true.', }) @ApiOkResponse({ type: CommandResultDto }) @ApiCommandErrors() publish( @CurrentUser() me: User, @Param('releaseId') releaseId: string, @Body() body: PublishReleaseDto, ) { return this.releases.publish(me.id, releaseId, body.force); } @Post('releases/:releaseId/archive') @HttpCode(200) @ApiOperation({ summary: 'Archive a planned release (its tickets leave it)' }) @ApiOkResponse({ type: CommandResultDto }) @ApiCommandErrors() archive(@CurrentUser() me: User, @Param('releaseId') releaseId: string) { return this.releases.archive(me.id, releaseId); } }