Files
task-stream/docs/03-api.md
Josep Piguillem 50d3ffd1dd feat: sprints with burndown and carry-over
Plan sprints with dates and a goal, run one at a time per project, and
put tickets in them from the ticket page. The Sprints tab follows the one
under way with its progress and a burndown in tickets or points against
the ideal line. Closing a sprint sends what was left to the next one or
back to the backlog; the board can be filtered by sprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:59:01 +02:00

37 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)[]
    settings: estados · transiciones · tipos · áreas
    addMember() changeMemberRole() removeMember() updateDetails() updateSettings()
  }
  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.

Proyectos públicos

Con isPublic (al crear, o después con updateProject / PATCH /api/projects/{project}, propietario o admin), cualquiera puede leer el proyecto, también sin token: el proyecto y sus miembros, tablero, tickets, comentarios, historial, releases (progreso y burnup), actividad, seguimiento y estadísticas. Escribir sigue siendo cosa de los miembros según su rol (sin token: 401; con token pero sin ser miembro: 403). Un usuario con sesión que no es miembro lee igual que un anónimo. Las subscriptions exigen sesión; con ella, también se pueden seguir proyectos públicos.

publicProjects (GraphQL) y GET /api/explore/projects (REST, sin token) los listan. Al volver a privado, quien no es miembro vuelve a recibir not found. Los enlaces a tickets de otros proyectos solo se muestran si el visitante puede leer esos proyectos.

Borrar un proyecto

Solo el propietario, escribiendo la clave del proyecto para confirmar (deleteProject(projectId, confirmKey) o DELETE /api/projects/{project}?confirm=CLAVE). El proyecto desaparece al momento y, a continuación, una saga borra sus tickets (y sus comentarios), sus releases y los enlaces que tickets de otros proyectos tenían hacia los suyos (ver Arquitectura). La clave queda libre para un proyecto nuevo. Los eventos se quedan en el event store como auditoría, pero desde la API no se puede deshacer.

Configuración por proyecto

Cada proyecto tiene sus estados, transiciones, tipos de ticket y áreas (Project.settings). Todos empiezan con los valores por defecto de abajo, y el propietario o un admin los cambia con updateProjectSettings (o PUT /api/projects/{project}/settings); el cambio es un evento más (ProjectSettingsChangedEvent).

Valor Campos Por defecto
Estado id, name, category, color, requiresReason, archived (el orden es el del tablero) BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, READY_FOR_QA, IN_QA, QA_FAILED, DONE, CANCELLED
Tipo id, name, icon, color, archived BUG, FEATURE, UI_CHANGE, TASK, QA
Área id, name, archived FRONTEND, BACKEND, MOBILE, QA, DESIGN, DEVOPS

La categoría de un estado dice qué significa, se llame como se llame: TODO (pendiente), IN_PROGRESS, QA, DONE y CANCELLED. Releases («todo terminado» = DONE o CANCELLED), «Mi trabajo», el seguimiento y las estadísticas trabajan con categorías, así que siguen funcionando con estados propios. Los tickets guardan el id del estado y, en el read model, su categoría (Issue.statusCategory).

Reglas:

  • No se borra lo que ya existe: se archiva. Tickets, historial y estadísticas siguen apuntando a ese id. Un valor archivado no se ofrece para tickets nuevos ni recibe transiciones; los tickets que ya lo tienen lo conservan.
  • Los ids son estables (A-Z, 0-9, _, hasta 30, empezando por letra); los nombres, de 1 a 40 caracteres, se pueden cambiar cuando se quiera.
  • Hace falta al menos un estado activo de categoría TODO (los tickets nuevos entran en el primero), uno DONE y un tipo activo.
  • Las transiciones solo pueden apuntar a estados que existen y nunca de un estado a sí mismo.
  • Colores: slate, stone, violet, blue, sky, teal, green, amber, orange, red, pink. Iconos: bug, feature, palette, task, qa, flag, star-fill, rocket, document, light-bulb, key, support.

Los proyectos creados antes de que existiera la configuración usan los valores por defecto (sus eventos no la llevan). Al arrancar, la API rellena la categoría de los tickets del read model que no la tienen.

Workflow por defecto

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 (requiresReason), 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 devuelve los estados activos a los que puede pasar el ticket según el workflow de su proyecto.

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/….
  • Adjuntos: los suben los miembros (no los lectores), hasta ATTACHMENTS_MAX_MB (25 MB) por archivo y 50 por ticket; pueden ir en el ticket o bajo uno de sus comentarios (commentId). Quita uno quien lo subió o un admin. Los bytes se guardan primero y el evento después, así que nunca queda un adjunto apuntando a un archivo que no existe; si el evento no se guarda, el archivo se borra. Al borrar un ticket se borran sus archivos.
  • Épicas y subtareas: un ticket puede colgar de otro del mismo proyecto (parent), hasta tres niveles (épica → ticket → subtarea) y sin bucles; se comprueba sobre el event store, no sobre el read model. Una «épica» es cualquier ticket de primer nivel del que cuelgan otros (epics(projectId) da su progreso: terminados = hechos o cancelados, y puntos). Si se borra el padre, sus hijos vuelven al primer nivel.
  • Orden del backlog: cada ticket tiene un rank (texto que se compara carácter a carácter). Mover uno (rankIssue, PUT /api/issues/{issue}/rank) solo reescribe ese ticket: recibe un rango entre el de quien queda encima y el de quien queda debajo, leídos del event store; si entretanto el orden cambió, la API lo rechaza y la web recarga. Un ticket que nunca se movió ocupa su orden de creación (IssueRankedEvent no existe para él). Las columnas del tablero y issues(filter: { sort: RANK }) siguen este orden.
  • Sprints: los planifica y gestiona quien administra el proyecto (owner o admin); mete tickets cualquier miembro. Un sprint va de PLANNED a ACTIVE (uno solo en curso por proyecto) y a CLOSED; dura como mucho 90 días y un sprint en curso conserva su fecha de inicio. Un sprint cerrado ya no acepta ni pierde tickets. Al cerrarlo se elige adónde van los tickets sin terminar: a otro sprint abierto del proyecto o al backlog; los terminados se quedan como registro. Solo se borran sprints que no han empezado (sus tickets vuelven al backlog).
  • Vistas guardadas: filtros del tablero con nombre. Las guardan los miembros del proyecto (los lectores, solo para sí); compartir una la hace visible a todo el proyecto. Borra una su autor, o un admin si es compartida; como mucho 50 por persona y proyecto. Son preferencias, no historia: viven en MongoDB (saved_filters), fuera del event store, y se borran con el proyecto.
  • Checklist: hasta 100 pasos de hasta 300 caracteres por ticket; los cambia quien puede editar el ticket.

Notificaciones

Se derivan de los eventos (NotificationsEventHandler) y se guardan por persona en MongoDB: no son estado de ningún agregado, solo cada uno lee y descarta las suyas.

Cuándo A quién Tipo
Se crea un ticket asignado o se asigna La persona asignada ASSIGNED
Un ticket cambia de estado Quien lo tiene asignado y quien lo abrió STATUS_CHANGED
Un ticket vuelve de QA sin pasar a hecho Los mismos, con el motivo QA_FAILED
Alguien comenta Quien lo tiene asignado y quien lo abrió COMMENTED
Te añaden a un proyecto La persona añadida ADDED_TO_PROJECT

Nadie recibe aviso de lo que hizo él mismo, y cada persona recibe uno por evento: la primera razón de la tabla gana (una mención antes que «comentó en tu ticket», y eso antes que «un ticket que sigues»).

Menciones: se escriben @[Nombre](id-de-usuario) (la web las inserta al escribir @); el id dice a quién avisar y el nombre es lo que se ve, así que renombrar a alguien no rompe comentarios antiguos. Solo avisan a miembros del proyecto. Seguir: cualquier miembro, también los lectores, puede seguir un ticket (watchIssue/unwatchIssue, eventos IssueWatchedEvent/IssueUnwatchedEvent), y quien comenta pasa a seguirlo (CommentersFollowSaga). El id es <agregado>:<versión>:<persona>, así que reentregar un evento no duplica avisos. Llegan en vivo por la subscription notificationAdded y, si la instalación tiene SMTP_URL, también por correo a quien lo active (email, desactivado por defecto); un correo que falla se registra y no bloquea el stream.

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, ProjectSettingsChangedEvent, ProjectMemberAddedEvent, ProjectMemberRoleChangedEvent, ProjectMemberRemovedEvent, ProjectDeletedEvent
Issue IssueCreatedEvent, IssueUpdatedEvent, IssueStatusChangedEvent, IssueAssignedEvent, IssueLinkedEvent, IssueUnlinkedEvent, IssueReleaseChangedEvent, IssueAttachmentAddedEvent, IssueAttachmentRemovedEvent, IssueWatchedEvent, IssueUnwatchedEvent, IssueParentChangedEvent, IssueRankedEvent, IssueSprintChangedEvent, IssueChecklistItemAddedEvent, IssueChecklistItemUpdatedEvent, IssueChecklistItemRemovedEvent, IssueDeletedEvent
Comment CommentAddedEvent, CommentEditedEvent, CommentDeletedEvent
Sprint SprintCreatedEvent, SprintUpdatedEvent, SprintStartedEvent, SprintClosedEvent, SprintDeletedEvent
Release ReleaseCreatedEvent, ReleaseUpdatedEvent, ReleaseShippedEvent, ReleaseArchivedEvent, ReleaseDeletedEvent

IssueStatusChangedEvent lleva además meta con el nombre y la categoría de los dos estados en el momento del cambio, para que el historial y las estadísticas no cambien si luego se renombra o recategoriza un estado.

Estadísticas

Una proyección más (issue_timeline en MongoDB) guarda cada paso de cada ticket: a qué estado pasó, con qué categoría, quién lo tenía asignado, y cuándo entró o salió de una release. Las estadísticas se calculan sobre ella con funciones puras (application/src/lib/analytics):

Métrica Definición
Creados vs. resueltos Por semana (lunes, UTC): tickets creados y tickets que entraron en una categoría DONE
Flujo acumulado Tickets en cada estado al final de cada día
Tiempo de ciclo Del primer paso a IN_PROGRESS o QA hasta DONE; media y percentil 85
Tiempo de entrega De la creación hasta DONE; media y percentil 85
Throughput Tickets resueltos por semana y por la persona que los tenía asignados
QA Salidas de un estado QA: a DONE cuenta como superado; a cualquier otro sitio salvo CANCELLED, como fallido
Burndown de un sprint Por día del sprint: tickets en él (sin cancelados), cuántos siguen abiertos y sus puntos; línea ideal hasta cero
Burnup de una release Por día, desde que se creó hasta que se publica (o hoy, máx. 180 días): tickets en ella (sin cancelados) y hechos

Los tickets anteriores a esta proyección solo tienen su creación y su estado actual; para tener su historia completa, reconstruye el read model (ver Despliegue).

3.2 GraphQL

Las operaciones requieren Authorization: Bearer <token de Pireon> (en subscriptions, en el connection_init), salvo las lecturas de proyectos públicos (project, issue, issues, release, activity, publicProjects y sus campos), que funcionan también sin token.

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
myIssues(assignedToMe, statuses, categories, limit, offset) «Mi trabajo»: tickets de todos mis proyectos, asignados a mí (por defecto) o de cualquiera (p. ej. categories: [QA])
upcomingReleases(limit) Releases planificadas de mis proyectos, por fecha objetivo
publicProjects Proyectos públicos (sin token)
project(idOrKey) Un proyecto (mío, o público) con su settings. Campos calculados: board(releaseId, assigneeId, unassigned, area, types, priorities, labels, search, perColumn), stats, analytics(days), releases
issues(filter, limit, offset) Búsqueda de tickets (estado, tipo, área, prioridad, persona, release, etiquetas, ticket padre, texto); sort: RANK para el orden del backlog
sprints(projectId), sprint(id) Sprints del proyecto (en curso, planificados, cerrados). Campos calculados: progress, burndown, issues
savedFilters(projectId) Mis vistas guardadas del tablero y las compartidas con el proyecto
epics(projectId) Épicas del proyecto (tickets de primer nivel de los que cuelgan otros) con el progreso de sus hijos
issue(idOrKey) Un ticket. Campos calculados: allowedTransitions, linkedIssues, comments, history
release(id) Una release. Campos calculados: progress, burnup, 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, updateProjectSettings, deleteProject(confirmKey), addProjectMember, changeProjectMemberRole, removeProjectMember
Tickets createIssue, updateIssue, changeIssueStatus, assignIssue, linkIssues, unlinkIssues, setIssueRelease, setIssueParent, rankIssue, addChecklistItem, updateChecklistItem, removeChecklistItem, watchIssue, unwatchIssue, removeAttachment (subir: por REST)
Comentarios addComment, editComment, deleteComment
Sprints createSprint, updateSprint, startSprint, closeSprint(carryOverToSprintId), deleteSprint, setIssueSprint
Vistas saveFilter(input) (devuelve el id), deleteSavedFilter(id)
Avisos markNotificationsRead(ids), updateNotificationSettings(email)
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
sprintChanged(projectId) Se planifica, cambia, empieza, cierra o borra un sprint (sprint es null al borrar)
projectChanged(projectId) Cambian los datos, la configuración 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

Estados, tipos y áreas son texto (ids de la configuración del proyecto), no enums: van entre comillas.

mutation ($settings: ProjectSettingsInput!) {
  # La configuración entera (la que devuelve project.settings, cambiada).
  updateProjectSettings(projectId: "…", settings: $settings) {
    version
  }
}

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
      }
    }
    analytics(days: 90) {
      createdVsResolved {
        week
        created
        resolved
      }
      cycleTime {
        averageDays
        p85Days
      }
      qa {
        failureRate
      }
    }
  }
  release(id: "…") {
    burnup {
      dates
      scope
      done
    }
  }
  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, el webhook, explore y las lecturas de proyectos públicos, exigen Authorization: Bearer <token de Pireon> (el AuthGuard global, igual que en GraphQL). En el documento OpenAPI, las rutas donde el token es opcional lo indican con un requisito de seguridad vacío ({}). 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/DELETE /api/projects/{project} (DELETE …?confirm=CLAVE), PUT …/settings, …/board, …/epics, …/stats, …/analytics?days=, …/activity, …/members (POST, PATCH, DELETE)
Tickets GET /api/me/issues (mi trabajo), GET/POST /api/projects/{project}/issues, GET/PATCH /api/issues/{issue}, PUT …/status, PUT …/assignee, PUT …/release, PUT/DELETE …/watch, PUT …/parent, PUT …/rank, POST …/checklist, PATCH/DELETE …/checklist/{item}, POST/DELETE …/links, GET/POST …/comments, GET …/history
Comentarios PATCH/DELETE /api/comments/{commentId}
Sprints GET/POST /api/projects/{project}/sprints, GET/PATCH/DELETE /api/sprints/{sprint}, POST …/start, POST …/close, GET …/progress, …/burndown, …/issues, PUT /api/issues/{issue}/sprint
Vistas GET/POST /api/projects/{project}/filters, DELETE /api/filters/{id}
Avisos GET /api/me/notifications, POST /api/me/notifications/read, GET/PUT /api/me/notification-settings
Adjuntos POST /api/issues/{issue}/attachments (multipart: file y opcional commentId), GET/DELETE /api/issues/{issue}/attachments/{id}
Releases GET /api/me/releases (próximas), GET/POST /api/projects/{project}/releases, GET/PATCH /api/releases/{id}, …/progress, …/burnup, …/issues, POST …/publish, POST …/archive
Explorar GET /api/explore/projects (proyectos públicos, sin token)
Sistema GET /api/health, GET /api/auth/config, POST /api/auth/token, POST /api/webhooks/pireon

Los archivos van por REST porque GraphQL no está hecho para bytes; la lista de adjuntos de un ticket llega con el ticket (attachments, en GraphQL y en REST). La descarga sirve en línea solo imágenes, vídeo, audio, PDF y texto plano, y todo con X-Content-Type-Options: nosniff y una CSP sandbox: un HTML o un SVG subido se descarga, nunca se ejecuta en el origen de la API.

Los filtros de tickets aceptan status (ids del proyecto) y category (?category=QA), repetidos o separados por comas. 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.