Files
task-stream/docs/03-api.md
2026-10-03 13:40:26 +02:00

12 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

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.