Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
# `@suaveplan/api-graphql-apollo` — Apollo Server GraphQL engine adapter
**Package**: `@suaveplan/api-graphql-apollo`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 7 (GraphQL + WebSocket / SSE engines) — see umbrella `openspec/changes/api-plugin-foundation/`
## Why
Per design doc §10.5 the foundation ships GraphQL Yoga as the default engine but offers Apollo Server as an alternative for shops standardised on Apollo's broader feature set — Apollo Studio integration, persisted-query manifest support, the @apollo/federation tooling, and a larger plugin ecosystem (including operation-cost analyzers and request-coalescing). Both engines MUST be host-config swappable, which is only meaningful if both implement the same `api-graphql` contract and pass the same conformance suite.
Without this adapter the foundation has only one GraphQL engine, undermining the §10.5 "engine choice" guarantee and forcing Apollo-standardised teams to fork the foundation.
## What changes
Net-new package depending on `@suaveplan/api-types`, `@suaveplan/api-graphql`, `@suaveplan/api-router`, `@suaveplan/api-ws`, `@suaveplan/error`, `@suaveplan/logger`, `@suaveplan/opentelemetry`, `@apollo/server` (peer), `graphql` (peer), `@apollo/server-plugin-landing-page-disabled` (peer), and `zod`.
- ADDED: `createApolloGraphqlEngine(opts)` — factory returning the engine implementing `@suaveplan/api-graphql`'s engine contract.
- ADDED: `ApolloGraphqlOptions` — Zod-validated config: `endpoint` (default `/graphql`), `sdlEndpoint` (default `/graphql/sdl`), `wsEndpoint` (default `/graphql/ws`), `uiEndpoint` (default `/graphql/ui`), `introspection` (default `false`), `persistedQueriesPath`, `studioReporting`, `maxQueryDepth`, `maxQueryComplexity`, `formatError`.
- ADDED: Endpoint mounts (identical to Yoga): `POST /graphql`, `GET /graphql/sdl`, `WS /graphql/ws`, `GET /graphql/ui` (proxied to wired UI plugin).
- ADDED: Apollo-specific features behind config flags:
- Persisted-query manifest (`persistedQueriesPath`).
- Apollo Studio usage reporting (`studioReporting`).
- Landing-page disabled by default (no GraphiQL fallback — same rule as Yoga: UI is opt-in via separate package).
- ADDED: Subscriptions — same `graphql-ws` over `api-ws` bridge as Yoga.
- ADDED: Per-request DataLoader scoping (identical to Yoga).
- ADDED: `GRAPHQL_APOLLO_*` error codes: `GRAPHQL_APOLLO_PARSE_FAILED`, `GRAPHQL_APOLLO_VALIDATION_FAILED`, `GRAPHQL_APOLLO_DEPTH_EXCEEDED`, `GRAPHQL_APOLLO_COMPLEXITY_EXCEEDED`, `GRAPHQL_APOLLO_SCHEMA_BUILD_FAILED`, `GRAPHQL_APOLLO_SUBSCRIPTION_TRANSPORT_UNAVAILABLE`, `GRAPHQL_APOLLO_TIMEOUT`, `GRAPHQL_APOLLO_INTROSPECTION_DENIED`, `GRAPHQL_APOLLO_PERSISTED_QUERY_NOT_FOUND`, `GRAPHQL_APOLLO_RESOLVER_THREW`.
- ADDED: OTel instruments: same surface as Yoga (`graphql.requests_total`, `graphql.request_duration_ms`, `graphql.resolver_duration_ms`, `graphql.subscriptions_active`, `graphql.schema_rebuilds_total`) — the metric names are deliberately engine-agnostic so dashboards survive an engine swap.
- ADDED: `./testing` subpath — re-export of `runGraphqlEngineConformance` bound to `createApolloGraphqlEngine`.
## Out of scope
- The abstract `api-graphql` contract.
- Apollo Federation (subgraph composition across services) — single-process schema federation across plugins is in scope; cross-service subgraph composition is a separate concern.
- The GraphiQL or Altair UI packages.
- Yoga-specific concerns.
## Source mapping
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| Alternative engine to Yoga | §10.5 |
| Same `api-graphql` contract | §10.5 |
| Same conformance suite passes | §10.5 ("Both implement the same `api-graphql` contract; switching is host-config only.") |
| Endpoint set identical to Yoga | §10.6 |
| Subscriptions over `api-ws` | §10.4 |
| Federation across plugins | §10.2 |
| Engine-agnostic OTel metric names | §12.3 (host emits for free) + §10.5 (swappable) |
## Verification approach
- `src/engine/apollo-engine.test.ts` — schema build, request execution, masked errors, depth/complexity guards.
- `src/engine/apollo-engine.spec-conformance.test.ts` — runs `runGraphqlEngineConformance("apollo", createApolloGraphqlEngine)`; MUST pass the exact same invariants as `api-graphql-yoga`.
- `src/federation/federation.test.ts` — same federation semantics; collision raises `GRAPHQL.SCHEMA_COLLISION`.
- `src/subscriptions/subscriptions.test.ts` (integration, 30000ms) — `graphql-ws` over `api-ws-broker-in-memory`.
- `src/persisted-queries/persisted-queries.test.ts` — Apollo-specific: persisted-query manifest lookup; unknown id raises `GRAPHQL_APOLLO_PERSISTED_QUERY_NOT_FOUND`.
- `tests/integration/engine-swap.test.ts` (30000ms) — boots the same plugin set under Yoga then under Apollo; asserts identical query results, identical SDL output, identical metric names (operator dashboards survive the swap).
## Migration impact
None — additive only.

View File

@@ -0,0 +1,127 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: api-graphql-apollo (api-graphql-apollo)
**Package**: `@suaveplan/api-graphql-apollo`
**Status**: PROPOSED
> Every requirement carries a `**Verified by:**` line.
## ADDED Requirements
### Requirement: Implements the abstract GraphQL engine contract
The package SHALL export `createApolloGraphqlEngine(opts)` producing an engine value that implements the engine contract from `@suaveplan/api-graphql`. The conformance suite `runGraphqlEngineConformance` MUST pass against this adapter with the same invariants required of `api-graphql-yoga`.
#### Scenario: conformance suite passes
- **WHEN** `runGraphqlEngineConformance("apollo", createApolloGraphqlEngine)` runs
- **THEN** every invariant passes
**Verified by:** `src/engine/apollo-engine.spec-conformance.test.ts::"createApolloGraphqlEngine passes runGraphqlEngineConformance"`
---
### Requirement: Endpoint set identical to Yoga adapter
The adapter SHALL mount the same four endpoints (`POST /graphql`, `GET /graphql/sdl`, `WS /graphql/ws`, `GET /graphql/ui`) at the same default paths with the same configurability. The `/graphql/ui` endpoint MUST return 404 when no UI plugin is wired — the adapter MUST NOT embed Apollo's landing page as a default (UI is opt-in via a separate package, identical to Yoga's stance).
#### Scenario: endpoint set matches Yoga
- **WHEN** the engine mounts on `api-router-in-memory`
- **THEN** the four endpoints respond at the same paths as the Yoga adapter does
#### Scenario: landing page disabled by default
- **WHEN** no UI plugin is wired
- **THEN** `GET /graphql/ui` returns 404 (NOT an Apollo landing page)
**Verified by:** `src/endpoints/http.test.ts::"endpoint set matches Yoga at default paths"` AND `src/endpoints/ui-proxy.test.ts::"/graphql/ui returns 404 when no UI plugin is wired"`
---
### Requirement: Federation across plugins with collision detection
The engine SHALL accept schema contributions from multiple plugins and merge them into one executable schema. `extend type Query` from multiple plugins MUST union. Same-field collisions without `graphql.namespace` MUST fail with `GRAPHQL.SCHEMA_COLLISION` (the canonical code, NOT a `GRAPHQL_APOLLO_*` code).
#### Scenario: two plugins extending Query both expose fields
- **WHEN** plugin A and plugin B each extend `Query`
- **THEN** both contribute fields to the merged schema
#### Scenario: collision fails at build
- **WHEN** two plugins contribute the same field on the same type
- **THEN** schema build fails with `GRAPHQL.SCHEMA_COLLISION`
**Verified by:** `src/federation/federation.test.ts::"two plugins extending Query both expose their fields"` AND `src/federation/federation.test.ts::"same-field collision fails with GRAPHQL.SCHEMA_COLLISION"`
---
### Requirement: Subscriptions ride api-ws
Subscriptions SHALL use `graphql-ws` protocol over the host's wired `api-ws` engine, with subscription resolvers returning `AsyncIterable` driven by the event bus, identical to the Yoga adapter.
#### Scenario: tenant-scoped subscription delivery
- **WHEN** a subscriber for tenant T subscribes AND an event for tenant T is emitted
- **THEN** the subscriber receives the event AND a tenant-U subscriber does not
**Verified by:** `src/subscriptions/subscriptions.test.ts::"subscription delivers tenant-scoped events only to matching tenants"`
---
### Requirement: Persisted-query manifest support
When `ApolloGraphqlOptions.persistedQueriesPath` is set, the engine SHALL accept `{ id }` in place of `{ query }` on `POST /graphql` and resolve `id` against the loaded manifest. An unknown id MUST fail with `GRAPHQL_APOLLO_PERSISTED_QUERY_NOT_FOUND`.
#### Scenario: known id resolves to its query
- **WHEN** `POST /graphql` arrives with `{ id: "abc123" }` AND `abc123` is in the manifest
- **THEN** the engine executes the manifest's query for that id
#### Scenario: unknown id is rejected
- **WHEN** the id is not in the manifest
- **THEN** the response is `GRAPHQL_APOLLO_PERSISTED_QUERY_NOT_FOUND`
**Verified by:** `src/persisted-queries/persisted-queries.test.ts::"known id resolves to its query"` AND `src/persisted-queries/persisted-queries.test.ts::"unknown id returns GRAPHQL_APOLLO_PERSISTED_QUERY_NOT_FOUND"`
---
### Requirement: Per-request DataLoader scoping
The engine SHALL create DataLoader instances per-request bound to `RequestContext`, with two concurrent requests receiving distinct loader instances.
#### Scenario: concurrent requests do not share loader caches
- **WHEN** R1 and R2 both invoke a loader-using resolver
- **THEN** R1's cached value is not served to R2
**Verified by:** `src/loaders/loaders.test.ts::"two concurrent requests get distinct DataLoader instances"`
---
### Requirement: OTel surface uses engine-agnostic metric names
The engine SHALL emit `graphql.requests_total`, `graphql.request_duration_ms`, `graphql.resolver_duration_ms`, `graphql.subscriptions_active`, and `graphql.schema_rebuilds_total` — the same metric names emitted by the Yoga adapter. Operators MUST be able to swap engines without renaming dashboards.
#### Scenario: metric names match Yoga's
- **WHEN** both engines run against the same plugin set under an in-memory `MeterProvider`
- **THEN** the set of metric names observed is identical
**Verified by:** `tests/integration/engine-swap.test.ts::"metric names are identical between Apollo and Yoga"`
---
### Requirement: Depth and complexity guards
The engine SHALL reject queries exceeding `maxQueryDepth` with `GRAPHQL_APOLLO_DEPTH_EXCEEDED` AND queries exceeding `maxQueryComplexity` with `GRAPHQL_APOLLO_COMPLEXITY_EXCEEDED`. Defaults: depth 10, complexity 1000.
#### Scenario: query exceeding depth is rejected
- **WHEN** a query has nested selection 11 levels deep AND `maxQueryDepth: 10`
- **THEN** execution fails with `GRAPHQL_APOLLO_DEPTH_EXCEEDED`
**Verified by:** `src/guards/depth-and-complexity.test.ts::"query exceeding depth limit rejected with GRAPHQL_APOLLO_DEPTH_EXCEEDED"`

View File

@@ -0,0 +1,77 @@
# Tasks — `@suaveplan/api-graphql-apollo`
> 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 (`git worktree list` then `git status` in each)
- [x] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [x] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Spec authoring
- [x] 1.1 Read design doc §10 end-to-end
- [x] 1.2 Read umbrella Stage 7 acceptance
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
- [x] 1.4 Read sibling `openspec/changes/api-graphql-yoga/` for cross-engine parity
- [x] 1.5 Draft `proposal.md`
- [x] 1.6 Draft `specs/api-graphql-apollo/spec.md`
- [x] 1.7 Every requirement carries a `**Verified by:**` line
## 2. Implementation
- [x] 2.1 Scaffold `packages/api/api-graphql-apollo/`
- [x] 2.2 `src/options/options.ts` — `ApolloGraphqlOptions` Zod schema
- [x] 2.3 `src/engine/apollo-engine.ts` — `createApolloGraphqlEngine(opts)`; implements engine contract
- [x] 2.4 `src/federation/federation.ts` — schema merge; collision raises `GRAPHQL.SCHEMA_COLLISION`
- [x] 2.5 `src/endpoints/http.ts` — mount `POST /graphql`, `GET /graphql/sdl`
- [x] 2.6 `src/endpoints/ui-proxy.ts` — `/graphql/ui` proxied to wired UI plugin
- [x] 2.7 `src/subscriptions/subscriptions.ts` — `graphql-ws` over `api-ws`
- [x] 2.8 `src/loaders/loaders.ts` — per-request DataLoader
- [x] 2.9 `src/persisted-queries/persisted-queries.ts` — manifest loader; lookup; eviction
- [x] 2.10 `src/studio-reporting/studio-reporting.ts` — optional Apollo Studio usage reporter; disabled by default
- [x] 2.11 `src/guards/depth-and-complexity.ts` — depth + complexity limits
- [x] 2.12 `src/instruments/instruments.ts` — engine-agnostic `graphql.*` metric names
- [x] 2.13 `src/error-codes.ts` — 10 `GRAPHQL_APOLLO_*` codes
- [x] 2.14 `./testing` subpath — re-export `runGraphqlEngineConformance` bound to Apollo factory
- [x] 2.15 `src/index.ts` — explicit named re-exports
## 3. Tests
- [x] 3.1 Unit tests, every `it(...)` `{ timeout: 5000 }`
- [x] 3.2 Conformance test — passes the SAME suite as Yoga
- [x] 3.3 Federation parity test
- [x] 3.4 Subscriptions integration (`{ timeout: 30000 }`)
- [x] 3.5 Persisted-query lookup tests
- [x] 3.6 Engine-swap integration test (`{ timeout: 30000 }`) — boots Yoga then Apollo, asserts identical SDL + identical metric names
- [x] 3.7 All tests import from `@suaveplan/testing/runner`
## 4. Documentation
- [x] 4.1 Sibling `.md` per implementation file
- [x] 4.2 `README.md` with `## Modules`; note positioning vs Yoga (Apollo for Studio / persisted-query / Apollo ecosystem; otherwise Yoga is faster)
## 5. Pre-archive verification
- [x] 5.1 `bun run check` — 0/0/0
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-graphql-apollo` — green
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-graphql-apollo` — 100/100/100/100
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-graphql-apollo` — exit 0
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-graphql-apollo` — green
## 6. Pre-archive 7-row checklist
- [x] 6.1 Generics threaded; `expectTypeOf` on every literal-type claim
- [x] 6.2 Zod schemas at every config boundary
- [x] 6.3 Every `GRAPHQL_APOLLO_*` code thrown
- [x] 6.4 Strict-mode tested (introspection on vs off; studio reporting on vs off)
- [x] 6.5 OTel instruments asserted; metric names match Yoga's
- [x] 6.6 Lifecycle dedup on schema rebuilds
- [x] 6.7 No retroactive tick
## 7. Archive
- [x] 7.1 Fold spec delta into `openspec/specs/api-graphql-apollo/spec.md`
- [x] 7.2 Move to `openspec/archive/<yyyy-mm-dd>-api-graphql-apollo/`
- [x] 7.3 Tick Stage-7 row in umbrella tasks
- [x] 7.4 Refresh `MANIFEST.md`