A REST adapter next to GraphQL (presenters/http) that calls the same application services, so validation, permissions and domain errors are the same: projects (members, board, stats, activity), issues (search, workflow, assignee, release, links, comments, history), comments, releases and users. Projects and issues can be addressed by id or key (WEB, WEB-12). - Swagger UI at /api/docs, document at /api/docs-json and apps/api/openapi.json (rewritten by `nx serve api`, like schema.gql) for client generation. - Every route but health/auth/webhook needs a Pireon token: the document declares Pireon OAuth2 (authorization code + PKCE, password) through the /api/auth/token proxy, and a bearer scheme for apps with their own client. - configureApp() shares main.ts setup with the e2e, which now also covers REST. - SWAGGER_ENABLED / OPENAPI_FILE config; docs 03/05 and library READMEs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
60 lines
1.8 KiB
TypeScript
60 lines
1.8 KiB
TypeScript
import { applyDecorators } from '@nestjs/common';
|
|
import {
|
|
ApiBadRequestResponse,
|
|
ApiConflictResponse,
|
|
ApiForbiddenResponse,
|
|
ApiNotFoundResponse,
|
|
ApiProperty,
|
|
ApiPropertyOptional,
|
|
ApiUnauthorizedResponse,
|
|
ApiUnprocessableEntityResponse,
|
|
} from '@nestjs/swagger';
|
|
|
|
/** Error body of the REST API (`DomainErrorFilter`, guards and validation). */
|
|
export class ApiErrorDto {
|
|
@ApiProperty({ example: 404 })
|
|
statusCode!: number;
|
|
|
|
@ApiPropertyOptional({
|
|
description: 'Domain error code',
|
|
enum: ['NOT_FOUND', 'FORBIDDEN', 'VALIDATION', 'CONFLICT', 'BUSINESS_RULE'],
|
|
})
|
|
code?: string;
|
|
|
|
@ApiProperty({
|
|
description: 'Human message (a list of messages for invalid input)',
|
|
oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
|
|
example: 'Issue not found',
|
|
})
|
|
message!: string | string[];
|
|
}
|
|
|
|
/** 400 / 401 / 403 / 404: what any authenticated read can answer. */
|
|
export const ApiReadErrors = () =>
|
|
applyDecorators(
|
|
ApiBadRequestResponse({ type: ApiErrorDto, description: 'Invalid input' }),
|
|
ApiUnauthorizedResponse({
|
|
type: ApiErrorDto,
|
|
description: 'Missing or rejected Pireon token',
|
|
}),
|
|
ApiForbiddenResponse({
|
|
type: ApiErrorDto,
|
|
description: 'Not a member of the project (or not allowed)',
|
|
}),
|
|
ApiNotFoundResponse({ type: ApiErrorDto, description: 'Not found' }),
|
|
);
|
|
|
|
/** Reads + 409 (concurrent change) and 422 (business rule) for commands. */
|
|
export const ApiCommandErrors = () =>
|
|
applyDecorators(
|
|
ApiReadErrors(),
|
|
ApiConflictResponse({
|
|
type: ApiErrorDto,
|
|
description: 'Someone changed it at the same time: retry',
|
|
}),
|
|
ApiUnprocessableEntityResponse({
|
|
type: ApiErrorDto,
|
|
description: 'Business rule (e.g. a transition the workflow forbids)',
|
|
}),
|
|
);
|