feat(api): REST API with OpenAPI for other apps, behind Pireon OAuth
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>
This commit is contained in:
@@ -1 +1,2 @@
|
||||
export * from './lib/filters/domain-error.filter';
|
||||
export * from './lib/http/api-errors';
|
||||
|
||||
59
libs/api/shared/presenters/src/lib/http/api-errors.ts
Normal file
59
libs/api/shared/presenters/src/lib/http/api-errors.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
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)',
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user