Files
task-stream/libs/api/tracker/presenters/src/lib/http/refs.ts
Josep Piguillem 341241a52f 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>
2026-10-03 15:26:47 +02:00

40 lines
1.2 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import {
IssuesService,
ProjectsService,
} from '@task-stream/api/tracker/application';
import { IssueReadModel, IssueWorkflow } from '@task-stream/api/tracker/domain';
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/**
* REST routes accept a project or ticket by id or by key (`WEB`, `WEB-12`),
* which is what other apps usually have. Keys are looked up in the read model;
* ids go straight to the command (the service still checks membership).
*/
@Injectable()
export class Refs {
constructor(
private readonly projects: ProjectsService,
private readonly issues: IssuesService,
) {}
async projectId(viewerId: string, ref: string): Promise<string> {
if (UUID.test(ref)) return ref;
return (await this.projects.findOne(viewerId, ref)).id;
}
async issueId(viewerId: string, ref: string): Promise<string> {
if (UUID.test(ref)) return ref;
return (await this.issues.findOne(viewerId, ref)).id;
}
}
/** The ticket as the REST API returns it: read model + allowed transitions. */
export function toIssueDto(issue: IssueReadModel) {
return {
...issue,
allowedTransitions: IssueWorkflow.allowedFrom(issue.status),
};
}