Per-project settings (owners/admins, «Configuración» tab):
- Statuses with category (TODO, IN_PROGRESS, QA, DONE, CANCELLED), colour and
"requires reason"; configurable transitions; ticket types (icon, colour) and
areas. Defaults are the previous fixed values; projects created before keep
them without migration.
- Values are archived, never deleted: tickets, history and statistics keep
referring to them. Validated in the domain (ProjectSettingsChangedEvent).
- Releases, board, tracking and "my work" use categories, so custom statuses
keep working. Status changes carry name/category metadata for history.
- GraphQL: status/type/area are strings (breaking for clients sending enum
literals); Project.settings, updateProjectSettings, statusCategory,
myIssues(categories). REST: PUT /projects/{project}/settings, category filters.
- Read model: statusCategory on issues (backfilled at startup).
Statistics («Estadísticas» tab):
- New issue_timeline projection; pure functions compute created vs resolved,
cumulative flow, cycle/lead time (avg + p85), throughput per person, QA
pass/fail and release burnup. GraphQL Project.analytics(days),
Release.burnup; REST /projects/{project}/analytics, /releases/{id}/burnup.
- Web: SVG charts with validated light/dark palette, crosshair tooltip,
keyboard navigation, legend and data table.
Home "Mi trabajo": my open tickets across projects grouped by category (QA
failures first), tickets waiting for QA, upcoming releases and "Ir a…".
Tests: domain/application specs, e2e scenario for settings and analytics,
facade tests. Docs, READMEs, schema.gql/openapi.json and Word/PDF updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
22 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.
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), unoDONEy 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_FAILEDyCANCELLEDexigen un motivo (requiresReason), que queda en el historial.- No se puede pasar a
DONEsaltándose QA salvo desdeIN_REVIEW(cambios que no requieren QA). Issue.allowedTransitionsdevuelve 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 (
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, ProjectSettingsChangedEvent, ProjectMemberAddedEvent, ProjectMemberRoleChangedEvent, ProjectMemberRemovedEvent |
| Issue | IssueCreatedEvent, IssueUpdatedEvent, IssueStatusChangedEvent, IssueAssignedEvent, IssueLinkedEvent, IssueUnlinkedEvent, IssueReleaseChangedEvent |
| Comment | CommentAddedEvent, CommentEditedEvent, CommentDeletedEvent |
| Release | ReleaseCreatedEvent, ReleaseUpdatedEvent, ReleaseShippedEvent, ReleaseArchivedEvent |
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 |
| 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
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 |
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 |
project(idOrKey) |
Un proyecto con su settings. Campos calculados: board(releaseId, assigneeId, area, perColumn), stats, analytics(days), 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, 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, 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, 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 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}, PUT …/settings, …/board, …/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, POST/DELETE …/links, GET/POST …/comments, GET …/history |
| Comentarios | PATCH/DELETE /api/comments/{commentId} |
| 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 |
| Sistema | GET /api/health, GET /api/auth/config, POST /api/auth/token, POST /api/webhooks/pireon |
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.