Files
suaveplan/openspec/archive/2026-05-26-api-graphql-yoga/proposal.md
2026-05-27 23:29:53 +01:00

5.6 KiB

@suaveplan/api-graphql-yoga — GraphQL Yoga engine adapter (default engine)

Package: @suaveplan/api-graphql-yoga Status: PROPOSED Type: new-package Stage: 7 (GraphQL + WebSocket / SSE engines) — see umbrella openspec/changes/api-plugin-foundation/

Why

GraphQL is a first-class engine alongside REST in the foundation (design doc §10). Plugins contribute types and resolvers via the abstract api-graphql Registry; one of the host's adapters federates a single merged schema across all plugins and dispatches requests. Per §10.5 GraphQL Yoga is the default engine — Bun-friendly, fastest for our usage, simpler operational story than Apollo Server. This package binds the abstract api-graphql contract to Yoga.

Without this adapter the abstract api-graphql contract has no runnable implementation, and the umbrella's Stage-7 acceptance ("api-plugin-example mounts a federated GraphQL schema across two plugins AND publishes a tenant-scoped WebSocket broadcast") can't be reached.

What changes

Net-new package depending on @suaveplan/api-types, @suaveplan/api-graphql (abstract contract), @suaveplan/api-router (mounts HTTP endpoints), @suaveplan/api-ws (subscription transport), @suaveplan/error, @suaveplan/logger, @suaveplan/opentelemetry, graphql (peer), graphql-yoga (peer), and zod.

  • ADDED: createYogaGraphqlEngine(opts) — factory returning the engine implementing @suaveplan/api-graphql's engine contract.
  • ADDED: YogaGraphqlOptions — Zod-validated config: endpoint (default /graphql), sdlEndpoint (default /graphql/sdl), wsEndpoint (default /graphql/ws), uiEndpoint (default /graphql/ui, only mounted if a UI plugin like api-graphql-ui-graphiql is wired), maskedErrors, landingPage, maxQueryDepth, maxQueryComplexity, parserCacheSize.
  • ADDED: Endpoint mounts:
    • POST /graphql — main execution endpoint.
    • GET /graphql/sdl — merged SDL across all federated plugin contributions.
    • WS /graphql/ws — graphql-ws protocol over the api-ws engine.
    • GET /graphql/ui — proxied to the wired UI plugin (this package does NOT mount GraphiQL/Altair itself; those are separate packages).
  • ADDED: Schema federation — wraps multiple plugin contributions into one merged schema via @graphql-tools/schema makeExecutableSchema. Collisions raise GRAPHQL.SCHEMA_COLLISION (PluginScope error code on @suaveplan/api-graphql's namespace).
  • ADDED: Subscriptions — bridges graphql-ws over the api-ws engine. Subscription resolvers return AsyncIterable driven by the event bus.
  • ADDED: Per-request DataLoader instances scoped to RequestContext, populated at pipeline step 7 and disposed at step 11 (design doc §10.3).
  • ADDED: GRAPHQL_YOGA_* error codes: GRAPHQL_YOGA_PARSE_FAILED, GRAPHQL_YOGA_VALIDATION_FAILED, GRAPHQL_YOGA_DEPTH_EXCEEDED, GRAPHQL_YOGA_COMPLEXITY_EXCEEDED, GRAPHQL_YOGA_SCHEMA_BUILD_FAILED, GRAPHQL_YOGA_SUBSCRIPTION_TRANSPORT_UNAVAILABLE, GRAPHQL_YOGA_TIMEOUT, GRAPHQL_YOGA_INTROSPECTION_DENIED, GRAPHQL_YOGA_PERSISTED_QUERY_NOT_FOUND, GRAPHQL_YOGA_RESOLVER_THREW.
  • ADDED: OTel instruments: graphql.requests_total { operation_type, operation_name, outcome }, graphql.request_duration_ms, graphql.resolver_duration_ms { type, field }, graphql.subscriptions_active gauge, graphql.schema_rebuilds_total.
  • ADDED: ./testing subpath — re-export of runGraphqlEngineConformance from api-graphql/testing bound to createYogaGraphqlEngine.

Out of scope

  • The abstract api-graphql contract, schema-collision error code namespace, Registry / Resolver / LoaderFactory types — those live in api-graphql (Stage 1).
  • The GraphiQL or Altair UI — separate packages api-graphql-ui-graphiql and api-graphql-ui-altair. This adapter only proxies /graphql/ui to whichever is wired.
  • Cross-plugin federation transport (the merge itself happens on the host); this adapter just consumes the already-merged schema descriptor.
  • Apollo-specific features (persisted-query manifests, Apollo Studio) — that's api-graphql-apollo.

Source mapping

Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.

Requirement Design doc section
Default engine for GraphQL §10.5
Endpoint set (POST /graphql, GET /graphql/sdl, WS /graphql/ws, GET /graphql/ui) §10.6
Federation via extend type Query { ... } across plugins §10.2
Subscriptions over api-ws via graphql-ws §10.4
Per-request DataLoader scoping §10.3
Same conformance suite as api-graphql-apollo §10.5 (last sentence)

Verification approach

  • src/engine/yoga-engine.test.ts — schema build, request execution, error masking, depth/complexity guards.
  • src/engine/yoga-engine.spec-conformance.test.ts — runs runGraphqlEngineConformance("yoga", createYogaGraphqlEngine); pins identical invariants to those required of api-graphql-apollo.
  • src/federation/federation.test.ts — two plugins each extend type Query; assert the merged schema serves both fields; assert collision raises GRAPHQL.SCHEMA_COLLISION.
  • src/subscriptions/subscriptions.test.ts (integration, 30000ms) — subscribes via graphql-ws over api-ws-broker-in-memory; broadcasts a tenant-scoped event; asserts delivery.
  • src/loaders/loaders.test.ts — per-request DataLoader instances; assert disposal after step 11.
  • tests/integration/end-to-end.test.ts (60000ms) — full pipeline: REST request → GraphQL request → subscription, all over Yoga, against an in-memory router + WS engine.

Migration impact

None — additive only.