Files
2026-05-27 23:29:53 +01:00

79 lines
5.3 KiB
Markdown

# `@suaveplan/api-graphql` — abstract GraphQL registry, resolver, loader, subscription contract
**Package**: `@suaveplan/api-graphql`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 1 (Contracts) — see umbrella `openspec/changes/api-plugin-foundation/`
## Why
GraphQL is a first-class engine alongside REST (design §10). Plugins mount GraphQL types + resolvers; the host federates a single schema across all plugins. Concrete engines (`api-graphql-yoga` default, `api-graphql-apollo` alternative) plug behind a stable contract so swapping engines is host-config only (design §10.5). This package owns that contract: the `Registry` (`addTypes` / `addResolvers` / `addLoader`), the `Resolver` shape, the `LoaderFactory` for DataLoaders, the `SubscriptionResolver` for GraphQL subscriptions over the WebSocket engine, and the federation rules (schema collision → `GRAPHQL.SCHEMA_COLLISION`, optional `graphql.namespace` wrapper).
`graphql` is added as a new `CapabilityId` — plugins request it via `requiredCapabilities` (design §10.1, §5.2).
## What changes
Net-new package. Pure contract — no concrete engine code. Some runtime: the federation merger (engine-agnostic SDL union with collision detection), Zod schemas for the subscription protocol, and the conformance harness.
- ADDED: `./registry` — abstract `GraphqlRegistry`: `addTypes(sdl: string)`, `addResolvers(map: ResolverMap)`, `addLoader<K, V>(name: string, batcher: BatchFn<K, V>)`.
- ADDED: `./resolver` — `Resolver<Parent, Args, Result, Caps>` typed against `RequestContext<Caps>`; `ResolverMap` keyed by GraphQL type name.
- ADDED: `./loader` — `LoaderFactory`: per-request DataLoader instance creation; lifecycle wired to step 7 (capability scoping) of the pipeline.
- ADDED: `./subscription` — `SubscriptionResolver` returning `AsyncIterable`; protocol marker for `graphql-ws` (subscriptions ride `api-ws` per §10.4).
- ADDED: `./federation` — `mergeSdl(contributions: readonly SdlContribution[]): MergedSchema`; collision detector → `GRAPHQL.SCHEMA_COLLISION` with the offending plugin pair + type/field path; `graphql.namespace` wrapper application.
- ADDED: `./directives` — `@auth(scope)` + `@rateLimit(rpm)` directive contracts (host-installed; engine adapters implement them).
- ADDED: `./engine` — abstract `GraphqlEngine` lifecycle interface (`buildSchema`, `executeQuery`, `executeSubscription`, `dispose`); concrete impls live in Stage 7 (`api-graphql-yoga`, `api-graphql-apollo`).
- ADDED: `./capability-id` — adds `"graphql"` to the `CapabilityId` union via the type-level merge with `@suaveplan/api-types`.
- ADDED: `./error-codes` — frozen `GRAPHQL_CODES`: `SCHEMA_COLLISION`, `RESOLVER_MISSING`, `LOADER_NOT_REGISTERED`, `DIRECTIVE_UNKNOWN`, `SUBSCRIPTION_TRANSPORT_UNAVAILABLE`, `NAMESPACE_REQUIRED`.
- ADDED: `./testing` — `runGraphqlEngineConformance(factory)` reusable harness.
## CapabilityId additions
This package contributes `"graphql"` to the `CapabilityId` union. Pinned in the spec; documented here so reviewers can compare against the union maintained by `api-types`.
## Out of scope
- Concrete engines (`api-graphql-yoga`, `api-graphql-apollo`) — Stage 7.
- GraphiQL / Altair UIs — Stage 7 (`api-graphql-ui-graphiql`, `api-graphql-ui-altair`).
- Persisted queries, Apollo Studio integration — engine-specific, Stage 7.
## Source mapping
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| `Registry.addTypes` / `addResolvers` / `addLoader` shape | §10.1 |
| Federation (`extend type Query` union; cross-plugin refs) | §10.2 |
| Pipeline integration (`@auth`, `@rateLimit` directives) | §10.3 |
| Subscriptions ride `api-ws` | §10.4 |
| Engine choice (Yoga default, Apollo alternative) | §10.5 |
| Collision → `GRAPHQL.SCHEMA_COLLISION` | §10.2 |
| `graphql.namespace` wrapper | §10.2 |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `runGraphqlEngineConformance` harness | Stage-5 conformance suite (§13.4) needs a contract-side signature. |
## Verification approach
- `src/registry/registry.test.ts` — `addTypes`/`addResolvers`/`addLoader` are accumulator-style; multiple calls compose; deduplication of identical contributions.
- `src/federation/merge-sdl.test.ts` — union of `extend type Query` from multiple plugins; collision detection.
- `src/federation/namespace.test.ts` — `graphql.namespace: "Billing"` wraps a plugin's contributions under `BillingQuery`/`BillingMutation`.
- `src/subscription/subscription.spec-conformance.test.ts` — `SubscriptionResolver` returns `AsyncIterable<T>` and ties to `api-ws` `SubscriptionBus`.
- `src/directives/directives.test.ts` — `@auth` and `@rateLimit` directive descriptors carry the contract.
- `src/engine/engine.spec-conformance.test.ts` — `GraphqlEngine` lifecycle interface present.
- `src/capability-id/capability-id.spec-conformance.test.ts` — `"graphql"` is in the `CapabilityId` union after augmentation.
- `tests/dep-graph.test.ts` — pins deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod`. No GraphQL engine deps.
## Migration impact
None — additive only.
## Impact
### Affected dependents
- (none) — [x] no action needed (reason: net-new package; concrete engines land in Stage 7)