5.9 KiB
Capability Spec: api-graphql-apollo
Package: @suaveplan/api-graphql-apollo
Status: SHIPPED 2026-05-26
Every requirement carries a
**Verified by:**line.
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/uireturns 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 /graphqlarrives with{ id: "abc123" }ANDabc123is 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"