Files
task-stream/docs/03-api.md
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

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_FAILED y CANCELLED exigen un motivo, que queda en el historial.
  • No se puede pasar a DONE saltándose QA salvo desde IN_REVIEW (cambios que no requieren QA).
  • Issue.allowedTransitions en 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 (OUTWARD en el origen, INWARD en el destino).
  • Releases: solo las PLANNED aceptan o pierden tickets; publicar con tickets sin terminar exige force: true.
  • Comentarios: solo el autor edita; borra el autor o un admin del proyecto.
  • Cambios de UI: figmaUrl debe ser un enlace https://…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 contra oauth.pireon.dev/oauth/authorize y, si PIREON_PASSWORD_GRANT=true, password. El intercambio de tokens va por POST /api/auth/token (Pireon no tiene CORS en /oauth/token y el client_secret se queda en el servidor). Para usar «Authorize» en Swagger UI, registra <api>/api/docs/oauth2-redirect.html como 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.