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>
15 KiB
3. API
API GraphQL en /graphql (HTTP para queries y mutations, WebSocket graphql-ws para subscriptions) más unos pocos
endpoints REST bajo /api. El esquema completo se genera en apps/api/schema.gql.
3.1 Modelo de dominio (contexto tracker)
classDiagram
class Project {
key: "TS"
name
members: (userId, role)[]
addMember() changeMemberRole() removeMember() updateDetails()
}
class Issue {
key: "TS-42"
type · area · priority · status
assigneeId · releaseId · labels · figmaUrl
links: (type, target)[]
changeStatus() assign() linkTo() unlink() setRelease() updateDetails()
}
class Comment {
body · authorId
edit() delete()
}
class Release {
name · status · targetDate
update() ship() archive()
}
Project "1" --> "*" Issue
Issue "1" --> "*" Comment
Project "1" --> "*" Release
Release "0..1" <-- "*" Issue
Issue "*" --> "*" Issue : links
Roles por proyecto
| Rol | Ver | Crear/modificar tickets, comentar | Gestionar miembros y releases |
|---|---|---|---|
OWNER (quien crea el proyecto) |
✅ | ✅ | ✅ (no se le puede quitar ni cambiar el rol) |
ADMIN |
✅ | ✅ | ✅ |
MEMBER |
✅ | ✅ | ❌ |
VIEWER |
✅ | ❌ | ❌ |
Cualquiera puede salir de un proyecto (salvo el propietario). Los no miembros reciben not found, para no revelar que el proyecto existe.
Workflow de un ticket
stateDiagram-v2
[*] --> BACKLOG
BACKLOG --> TODO
BACKLOG --> IN_PROGRESS
BACKLOG --> CANCELLED
TODO --> BACKLOG
TODO --> IN_PROGRESS
TODO --> CANCELLED
IN_PROGRESS --> TODO
IN_PROGRESS --> IN_REVIEW
IN_PROGRESS --> READY_FOR_QA
IN_PROGRESS --> CANCELLED
IN_REVIEW --> IN_PROGRESS
IN_REVIEW --> READY_FOR_QA
IN_REVIEW --> DONE
READY_FOR_QA --> IN_QA
READY_FOR_QA --> IN_PROGRESS
IN_QA --> DONE
IN_QA --> QA_FAILED
QA_FAILED --> IN_PROGRESS
QA_FAILED --> TODO
DONE --> TODO
DONE --> IN_PROGRESS
CANCELLED --> BACKLOG
QA_FAILEDyCANCELLEDexigen un motivo, que queda en el historial.- No se puede pasar a
DONEsaltándose QA salvo desdeIN_REVIEW(cambios que no requieren QA). Issue.allowedTransitionsen GraphQL devuelve los estados permitidos desde el actual.
Otras reglas
- Proyecto: clave de 2 a 10 caracteres alfanuméricos que empieza por letra y es única (los tickets se numeran
CLAVE-1,CLAVE-2…). - Asignación: solo a miembros del proyecto. Para añadir miembros, el usuario debe existir y estar activo en Pireon.
- Relaciones: nunca consigo mismo ni duplicadas; pueden cruzar proyectos si el actor puede ver el destino. Se
muestran en los dos tickets (
OUTWARDen el origen,INWARDen el destino). - Releases: solo las
PLANNEDaceptan o pierden tickets; publicar con tickets sin terminar exigeforce: true. - Comentarios: solo el autor edita; borra el autor o un admin del proyecto.
- Cambios de UI:
figmaUrldebe ser un enlacehttps://…figma.com/….
Eventos de dominio
Todo cambio es un evento guardado en el event store (stream = id del agregado). Llevan actorId, occurredAt,
aggregateId, projectId y aggregateVersion (la posición en el stream).
| Agregado | Eventos |
|---|---|
| Project | ProjectCreatedEvent, ProjectUpdatedEvent, ProjectMemberAddedEvent, ProjectMemberRoleChangedEvent, ProjectMemberRemovedEvent |
| Issue | IssueCreatedEvent, IssueUpdatedEvent, IssueStatusChangedEvent, IssueAssignedEvent, IssueLinkedEvent, IssueUnlinkedEvent, IssueReleaseChangedEvent |
| Comment | CommentAddedEvent, CommentEditedEvent, CommentDeletedEvent |
| Release | ReleaseCreatedEvent, ReleaseUpdatedEvent, ReleaseShippedEvent, ReleaseArchivedEvent |
3.2 GraphQL
Todas las operaciones requieren Authorization: Bearer <token de Pireon> (en subscriptions, en el connection_init).
Queries
| Query | Devuelve |
|---|---|
me |
El usuario autenticado, con sus roles en Pireon |
users(search, limit) |
Usuarios de Pireon (para asignar o añadir miembros) |
user(id) |
Un usuario |
myProjects |
Proyectos de los que soy miembro |
project(idOrKey) |
Un proyecto. Campos calculados: board(releaseId, assigneeId, area, perColumn), stats, releases |
issues(filter, limit, offset) |
Búsqueda de tickets (estado, tipo, área, prioridad, persona, release, etiquetas, texto) |
issue(idOrKey) |
Un ticket. Campos calculados: allowedTransitions, linkedIssues, comments, history |
release(id) |
Una release. Campos calculados: progress, issues |
activity(projectId, issueId?, limit, before?) |
Feed de actividad, del más reciente al más antiguo |
Mutations
Todas devuelven CommandResult { id, key, version }.
| Ámbito | Mutations |
|---|---|
| Proyectos | createProject, updateProject, addProjectMember, changeProjectMemberRole, removeProjectMember |
| Tickets | createIssue, updateIssue, changeIssueStatus, assignIssue, linkIssues, unlinkIssues, setIssueRelease |
| Comentarios | addComment, editComment, deleteComment |
| Releases | createRelease, updateRelease, publishRelease(force), archiveRelease |
Subscriptions
| Subscription | Se emite cuando |
|---|---|
issueChanged(projectId) |
Se crea o cambia cualquier ticket del proyecto (incluye el ticket completo y action = nombre del evento) |
commentChanged(issueId) |
Se añade, edita o borra un comentario (comment es null al borrar) |
activityAdded(projectId) |
Hay una nueva entrada en el feed |
releaseChanged(projectId) |
Cambia una release |
projectChanged(projectId) |
Cambian los datos o los miembros del proyecto |
myProjectsChanged |
Me añaden a un proyecto o me quitan de uno |
Solo los miembros pueden suscribirse a un proyecto; las conexiones sin token válido se cierran con el código 4403.
Ejemplos
mutation {
createProject(input: { key: "SHOP", name: "Checkout revamp" }) {
id
key
version
}
}
mutation {
createIssue(input: { projectId: "…", type: BUG, title: "El botón de pagar no responde en Safari", area: FRONTEND, priority: CRITICAL, assigneeId: "102", labels: ["safari"] }) {
id
key
version
}
}
mutation {
changeIssueStatus(input: { issueId: "…", status: QA_FAILED, reason: "Sigue fallando en Safari 17" }) {
version
}
}
mutation {
linkIssues(input: { issueId: "<FE>", type: DEPENDS_ON, targetIssueId: "<BE>" }) {
version
}
}
query {
project(idOrKey: "SHOP") {
stats {
total
open
byStatus {
key
count
}
byAssignee {
user {
name
}
count
}
}
board {
status
total
issues {
key
title
assignee {
name
}
}
}
releases {
name
status
progress {
percent
}
}
}
issue(idOrKey: "SHOP-1") {
status
allowedTransitions
links {
type
direction
issueKey
}
comments {
items {
author {
name
}
body
}
}
history {
summary
occurredAt
}
}
}
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'ws://localhost:3000/graphql',
connectionParams: { authorization: `Bearer ${token}` },
});
client.subscribe(
{
query: 'subscription($p: ID!) { issueChanged(projectId: $p) { action version issue { key status } } }',
variables: { p: projectId },
},
{ next: console.log, error: console.error, complete: () => {} },
);
Errores
Los errores de dominio llegan como errores GraphQL con extensions.code:
| Código | Significado |
|---|---|
UNAUTHENTICATED |
Falta el token o Pireon lo rechaza |
NOT_FOUND |
No existe o no eres miembro |
FORBIDDEN |
No tienes permiso (p. ej. un VIEWER modificando) |
VALIDATION |
Datos inválidos (clave de proyecto, URL de Figma…) |
BUSINESS_RULE |
Regla de negocio (transición no permitida, release bloqueada…) |
CONFLICT |
Otro cambio llegó antes (concurrencia optimista): reintenta |
3.3 REST con OpenAPI (para otras apps)
Los mismos casos de uso que GraphQL, como API REST documentada con OpenAPI 3.0, para que otras apps de la plataforma
la consuman o generen un cliente. Son otro adaptador de entrada (presenters/http) que llama a los mismos
*.service.ts: misma validación, permisos y errores.
| Swagger UI | /api/docs (con «Authorize» contra Pireon) |
| Documento OpenAPI | /api/docs-json (y /api/docs-yaml) |
| Fichero exportado | apps/api/openapi.json: lo reescribe nx serve api al arrancar, como schema.gql |
Autenticación: siempre a través de Pireon
Cualquier app o usuario que la consuma pasa por el OAuth de Pireon: todas las rutas, salvo health, auth y el
webhook, exigen Authorization: Bearer <token de Pireon> (el AuthGuard global, igual que en GraphQL). El documento
declara dos esquemas:
pireon-oauth(OAuth2): Authorization Code + PKCE contraoauth.pireon.dev/oauth/authorizey, siPIREON_PASSWORD_GRANT=true, password. El intercambio de tokens va porPOST /api/auth/token(Pireon no tiene CORS en/oauth/tokeny elclient_secretse queda en el servidor). Para usar «Authorize» en Swagger UI, registra<api>/api/docs/oauth2-redirect.htmlcomo redirect URI del cliente en Pireon.pireon(Bearer): para una app que ya tiene un token de Pireon, por ejemplo con su propio cliente OAuth.
Lo que cada usuario puede ver o cambiar depende de su rol en cada proyecto, como en la web.
Recursos
Proyectos y tickets se pueden indicar por id o por clave (WEB, WEB-12).
| Recurso | Rutas |
|---|---|
| Usuarios | GET /api/users/me, GET /api/users?search=, GET /api/users/{id} |
| Proyectos | GET/POST /api/projects, GET/PATCH /api/projects/{project}, …/board, …/stats, …/activity, …/members (POST, PATCH, DELETE) |
| Tickets | GET/POST /api/projects/{project}/issues, GET/PATCH /api/issues/{issue}, PUT …/status, PUT …/assignee, PUT …/release, POST/DELETE …/links, GET/POST …/comments, GET …/history |
| Comentarios | PATCH/DELETE /api/comments/{commentId} |
| Releases | GET/POST /api/projects/{project}/releases, GET/PATCH /api/releases/{id}, …/progress, …/issues, POST …/publish, POST …/archive |
| Sistema | GET /api/health, GET /api/auth/config, POST /api/auth/token, POST /api/webhooks/pireon |
Las escrituras responden { id, key?, version } en cuanto el evento se guarda; las lecturas salen del read model, que
se pone al día en milisegundos (un GET justo después puede devolver la versión anterior, o 404 tras crear). Para
tiempo real, las subscriptions de GraphQL.
Errores
{ statusCode, code?, message }: 400 entrada no válida (message es una lista), 401 sin token o token rechazado
por Pireon, 403/404 sin acceso, 409 cambio concurrente (reintentar) y 422 regla de negocio (p. ej. una
transición que el workflow no permite).
Generar un cliente en otra app
npx openapi-typescript apps/api/openapi.json -o src/task-stream-api.d.ts # tipos
# o un cliente completo: openapi-generator, orval, ng-openapi-gen…
Ver también Autenticación.