# 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`) ```mermaid 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 ```mermaid 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 ` (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 ```graphql 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: "", type: DEPENDS_ON, targetIssueId: "" }) { 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 } } } ``` ```ts 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 | Endpoint | Uso | | --------------------------- | ----------------------------------------------------------------------------------------------- | | `GET /api/health` | Estado de PostgreSQL, MongoDB y Redis (público) | | `GET /api/auth/config` | Configuración para iniciar sesión con Pireon desde el navegador (público) | | `POST /api/auth/token` | Proxy de `/oauth/token` de Pireon: `authorization_code`, `password` y `refresh_token` (público) | | `POST /api/webhooks/pireon` | Webhooks de Pireon firmados con HMAC (`user.deactivated`, `token.revoked`) | Ver [Autenticación](05-autenticacion.md).