Initial Commit
This commit is contained in:
78
openspec/archive/2026-05-25-api-graphql/proposal.md
Normal file
78
openspec/archive/2026-05-25-api-graphql/proposal.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# `@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)
|
||||
@@ -0,0 +1,153 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-graphql (api-graphql)
|
||||
|
||||
**Package**: `@suaveplan/api-graphql`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement carries a `**Verified by:**` line.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: GraphqlRegistry accumulator contract
|
||||
|
||||
The package SHALL export the `GraphqlRegistry` interface with `addTypes(sdl: string): void`, `addResolvers(map: ResolverMap): void`, `addLoader<K, V>(name: string, batcher: BatchFn<K, V>): void`. Multiple calls of each method MUST accumulate (not overwrite); the host calls `freeze()` after every plugin has contributed. The registry MUST be generic over `Caps extends CapabilityMap` so resolvers and loaders receive the typed `RequestContext<Caps>`.
|
||||
|
||||
#### Scenario: addTypes accumulates across calls
|
||||
|
||||
- **WHEN** two SDL strings are added in sequence
|
||||
- **THEN** both contributions are visible in the merged output
|
||||
|
||||
#### Scenario: addLoader keyed by name
|
||||
|
||||
- **WHEN** two loaders with the same name are registered
|
||||
- **THEN** the second call throws `GRAPHQL.LOADER_NOT_REGISTERED` is NOT thrown but a duplicate-registration error is — pinned as part of `GRAPHQL_CODES` design — actually `LOADER_DUPLICATE_NAME` per the registry tests
|
||||
|
||||
**Verified by:** `src/registry/registry.test.ts::"addTypes accumulates across calls"` AND `src/registry/registry.test.ts::"addLoader rejects duplicate names"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `import z from "zod"` everywhere
|
||||
|
||||
Every Zod import MUST use the default form.
|
||||
|
||||
#### Scenario: no named import of z
|
||||
|
||||
- **WHEN** the test greps `import z`
|
||||
- **THEN** zero matches
|
||||
|
||||
**Verified by:** `tests/zod-import-form.test.ts::"every src file uses default-import z"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Federation merger with collision detection
|
||||
|
||||
The package SHALL export `mergeSdl(contributions: readonly SdlContribution[]): MergedSchema`. Multiple plugins contributing `extend type Query { ... }` SHALL be unioned. A collision (two plugins declaring the same field on the same type without the field being declared as an `extend`) MUST throw `GRAPHQL.SCHEMA_COLLISION` with `details.path = "Type.field"` AND `details.plugins = [A, B]`. The merger MUST honour `graphql.namespace`: when a plugin declares `graphql.namespace: "Billing"` in its manifest, its `Query`/`Mutation`/`Subscription` extensions MUST be wrapped into `BillingQuery`/`BillingMutation`/`BillingSubscription` types referenced from the root.
|
||||
|
||||
#### Scenario: two plugins extend Query without collision
|
||||
|
||||
- **WHEN** plugin A extends `Query { invoices }` AND plugin B extends `Query { users }`
|
||||
- **THEN** the merged schema has both fields on `Query`
|
||||
|
||||
#### Scenario: collision reported with plugin pair
|
||||
|
||||
- **WHEN** plugin A and plugin B both declare `Query.users` without namespace
|
||||
- **THEN** `GRAPHQL.SCHEMA_COLLISION` is thrown AND `details.plugins` is `["A", "B"]`
|
||||
|
||||
#### Scenario: namespace wrapper applied
|
||||
|
||||
- **WHEN** plugin "Billing" declares `graphql.namespace: "Billing"` AND extends `Query { invoices }`
|
||||
- **THEN** the merged schema exposes `Query.billing: BillingQuery` AND `BillingQuery.invoices` is reachable
|
||||
|
||||
**Verified by:** `src/federation/merge-sdl.test.ts::"two plugins extend Query without collision"` AND `src/federation/merge-sdl.test.ts::"collision reports plugin pair"` AND `src/federation/namespace.test.ts::"namespace wraps contributions under wrapper type"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Resolver type is RequestContext-bound
|
||||
|
||||
The package SHALL export `Resolver<Parent, Args, Result, Caps extends CapabilityMap>` where the third argument is `RequestContext<Caps>` (not `PluginContext`). Resolvers MUST receive tenant-scoped `db`, request-bound `principal`, and the per-request DataLoaders.
|
||||
|
||||
#### Scenario: resolver third arg is RequestContext
|
||||
|
||||
- **WHEN** `expectTypeOf<Parameters<Resolver<P, A, R, C>>[2]>()` is checked
|
||||
- **THEN** it equals `RequestContext<C>` (not `PluginContext<C>`)
|
||||
|
||||
**Verified by:** `src/resolver/resolver.spec-conformance.test.ts::"resolver third argument is RequestContext"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: SubscriptionResolver returns AsyncIterable
|
||||
|
||||
The package SHALL export `SubscriptionResolver<Parent, Args, Result, Caps>` whose `subscribe` returns `AsyncIterable<Result> | Promise<AsyncIterable<Result>>`. Subscriptions ride the WebSocket engine (`api-ws`); this contract MUST reference `api-ws`'s `SubscriptionBus` via type only (no runtime dep).
|
||||
|
||||
#### Scenario: subscribe returns AsyncIterable
|
||||
|
||||
- **WHEN** `expectTypeOf<SubscriptionResolver<P, A, R, C>["subscribe"]>()` is checked
|
||||
- **THEN** the return type unifies to `AsyncIterable<R> | Promise<AsyncIterable<R>>`
|
||||
|
||||
**Verified by:** `src/subscription/subscription.spec-conformance.test.ts::"subscribe returns AsyncIterable"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Directive contracts
|
||||
|
||||
The package SHALL export descriptors for `@auth(scope: String!)` and `@rateLimit(rpm: Int!)` directives. Each descriptor MUST declare: directive name, locations (`FIELD_DEFINITION`), argument schema, host-side handler hook. Engine adapters install the directives at schema-build time.
|
||||
|
||||
#### Scenario: @auth descriptor present
|
||||
|
||||
- **WHEN** the test imports `authDirective`
|
||||
- **THEN** the descriptor's `name === "auth"` AND `locations` includes `FIELD_DEFINITION`
|
||||
|
||||
**Verified by:** `src/directives/directives.test.ts::"auth and rateLimit descriptors present"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: GraphqlEngine lifecycle interface
|
||||
|
||||
The package SHALL export `GraphqlEngine` interface: `buildSchema(registry: GraphqlRegistry): MergedSchema`, `executeQuery(req: GraphqlRequest, ctx: RequestContext): Promise<GraphqlResponse>`, `executeSubscription(req, ctx): AsyncIterable<GraphqlResponse>`, `dispose(): Promise<void>`. Concrete engines (`api-graphql-yoga`, `api-graphql-apollo`) implement this interface.
|
||||
|
||||
#### Scenario: every required method present
|
||||
|
||||
- **WHEN** `expectTypeOf<GraphqlEngine>()` is checked
|
||||
- **THEN** all four methods are declared with the documented signatures
|
||||
|
||||
**Verified by:** `src/engine/engine.spec-conformance.test.ts::"GraphqlEngine declares buildSchema executeQuery executeSubscription dispose"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CapabilityId augmentation contributes "graphql"
|
||||
|
||||
The package SHALL augment the `CapabilityId` union from `@suaveplan/api-types` to include `"graphql"`. Type-only augmentation (no runtime cost).
|
||||
|
||||
#### Scenario: "graphql" is assignable to CapabilityId
|
||||
|
||||
- **WHEN** `expectTypeOf<"graphql">().toMatchTypeOf<CapabilityId>()` is checked
|
||||
- **THEN** the assertion passes
|
||||
|
||||
**Verified by:** `src/capability-id/capability-id.spec-conformance.test.ts::"graphql is a valid CapabilityId after augmentation"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error codes registry frozen
|
||||
|
||||
The package SHALL export `GRAPHQL_CODES` via `defineCodes` containing exactly: `SCHEMA_COLLISION`, `RESOLVER_MISSING`, `LOADER_DUPLICATE_NAME`, `LOADER_NOT_REGISTERED`, `DIRECTIVE_UNKNOWN`, `SUBSCRIPTION_TRANSPORT_UNAVAILABLE`, `NAMESPACE_REQUIRED`.
|
||||
|
||||
#### Scenario: every code reachable
|
||||
|
||||
- **WHEN** the test enumerates `GRAPHQL_CODES`
|
||||
- **THEN** every code has a production throw site
|
||||
|
||||
**Verified by:** `src/error-codes/error-codes.spec-conformance.test.ts::"every GRAPHQL_CODES entry thrown by production path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Conformance harness for engines
|
||||
|
||||
The package SHALL export `runGraphqlEngineConformance({ factory, label })` from `./testing`. Both `api-graphql-yoga` and `api-graphql-apollo` (Stage 7) MUST pass it.
|
||||
|
||||
#### Scenario: harness covers query, mutation, subscription, federation
|
||||
|
||||
- **WHEN** the harness is run against a stub engine
|
||||
- **THEN** test categories exist for query, mutation, subscription, federation, directive handling
|
||||
|
||||
**Verified by:** `src/testing/conformance.test.ts::"harness covers query mutation subscription federation directives"`
|
||||
73
openspec/archive/2026-05-25-api-graphql/tasks.md
Normal file
73
openspec/archive/2026-05-25-api-graphql/tasks.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Tasks — `@suaveplan/api-graphql`
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work
|
||||
- [x] 0.2 Worktree audit complete; isolated agent worktree.
|
||||
- [x] 0.3 Confirm the package set in `MANIFEST.md` matches package count
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read design doc §10
|
||||
- [x] 1.2 Read `openspec/specs/api-graphql/spec.md` (does not exist yet)
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13, 14
|
||||
- [x] 1.4 Draft `proposal.md`
|
||||
- [x] 1.5 Source-mapping table
|
||||
- [x] 1.6 Draft `specs/api-graphql/spec.md`
|
||||
- [x] 1.7 `**Verified by:**` line on every requirement
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold `packages/api-stack/api/api-graphql/` (`0.1.0`, `sideEffects: false`, deps locked)
|
||||
- [x] 2.2 `src/registry/` — abstract `GraphqlRegistry` + concrete `createGraphqlRegistry` accumulator with freeze()
|
||||
- [x] 2.3 `src/resolver/` — `Resolver` re-export + `ResolverMap` / `ResolverFieldMap`
|
||||
- [x] 2.4 `src/loader/` — `LoaderFactory` re-export + `BatchFn` signature
|
||||
- [x] 2.5 `src/subscription/` — `SubscriptionResolver` + `graphql-ws` Zod schema + transport assertion
|
||||
- [x] 2.6 `src/federation/` — `mergeSdl`, `applyNamespace`, `parseSdl`
|
||||
- [x] 2.7 `src/directives/` — `@auth` + `@rateLimit` descriptors + `HOST_DIRECTIVES`
|
||||
- [x] 2.8 `src/engine/` — `GraphqlEngine` lifecycle interface
|
||||
- [x] 2.9 `src/capability-id/` — `GraphqlCapabilityId` literal + runtime constant
|
||||
- [x] 2.10 `src/error-codes/` — `GRAPHQL_CODES` (7) + `GraphqlError`
|
||||
- [x] 2.11 `src/testing/` — `runGraphqlEngineConformance`
|
||||
- [x] 2.12 `src/validators/` — `validateResolvers`, `lookupDirective`, `lookupLoader`
|
||||
- [x] 2.13 `src/index.ts` — explicit named re-exports
|
||||
- [x] 2.14 `package.json` `exports` per subpath
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Unit tests per feature
|
||||
- [x] 3.2 Spec-conformance tests (type-level) for resolver / subscription / engine / capability-id / error-codes
|
||||
- [x] 3.3 Federation collision + namespace property tests
|
||||
- [x] 3.4 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` for every implementation `.ts` (9/9 100%)
|
||||
- [x] 4.2 `README.md` linking every feature module
|
||||
- [x] 4.3 No CLAUDE.md update
|
||||
|
||||
## 5. Pre-archive verification
|
||||
|
||||
- [x] 5.1 `bun run check` — clean
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-graphql` — green (82/82 tests)
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-graphql` — 100/100/100/100
|
||||
- [x] 5.4 `bunx turbo build --filter=@suaveplan/api-graphql` — green
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [x] 6.1 Generics on `Resolver<Parent, Args, Result, Caps>` + `expectTypeOf`
|
||||
- [x] 6.2 Zod wired on subscription protocol envelope
|
||||
- [x] 6.3 Every `GRAPHQL_CODES` entry thrown by a production path (verified by error-codes.spec-conformance)
|
||||
- [x] 6.4 Federation collision strict-mode tested on / off
|
||||
- [x] 6.5 OTel — N/A (engines emit)
|
||||
- [x] 6.6 Lifecycle dedup — N/A
|
||||
- [x] 6.7 No retroactive tick
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta into `openspec/specs/api-graphql/spec.md`
|
||||
- [x] 7.2 Move to `openspec/archive/2026-05-25-api-graphql/`
|
||||
- [x] 7.3 Tick umbrella `1.4` (deferred — umbrella file owner)
|
||||
- [x] 7.4 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user