128 lines
5.9 KiB
Markdown
128 lines
5.9 KiB
Markdown
<!-- 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"`
|