first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
# @suaveplan/api-graphql-apollo — Apollo Server GraphQL engine adapter for the Suaveplan API plugin system. Implements the GraphqlEngine contract from @suaveplan/api-graphql and passes runGraphqlEngineConformance with the same invariants as the Yoga sibling. Adds Apollo-specific persisted-query manifest support, optional Apollo Studio usage reporting, depth + complexity guards, federated multi-plugin schema merging, per-request DataLoader scoping, engine-agnostic OTel instruments, and graphql-ws subscription bridging.
Stage-7 Apollo Server GraphQL engine adapter for the Suaveplan API
plugin system. Implements the `GraphqlEngine` contract from
`@suaveplan/api-graphql` and passes the conformance suite alongside
the Yoga sibling — switching engines is a host-config change.
<!-- begin:auto-docs -->
## Documentation tree
- [http](./src/endpoints/http.md) — `endpoints/http` mounts the two HTTP endpoints every GraphQL adapter must expose: `POST /graphql` for operation dispatc…
- [ui-proxy](./src/endpoints/ui-proxy.md) — `endpoints/ui-proxy` mounts the `/graphql/ui` endpoint. The Apollo engine never embeds Apollo's landing page — UI is op…
- [engine](./src/engine/apollo-engine.md) — `ApolloGraphqlEngine` is the concrete Apollo Server implementation of the `@suaveplan/api-graphql` engine contract. It…
- [apollo-engine](./src/engine/apollo-engine.md) — `ApolloGraphqlEngine` is the concrete Apollo Server implementation of the `@suaveplan/api-graphql` engine contract. It…
- [error-codes](./src/error-codes/error-codes.md) — `error-codes` defines the stable `GRAPHQL_APOLLO.*` namespace plus the `GraphqlApolloError` class consumed by every oth…
- [error-codes](./src/error-codes/error-codes.md) — `error-codes` defines the stable `GRAPHQL_APOLLO.*` namespace plus the `GraphqlApolloError` class consumed by every oth…
- [federation](./src/federation/federation.md) — `federation` is the engine's facade over the canonical `mergeSdl` function from `@suaveplan/api-graphql`. Apollo doesn'…
- [federation](./src/federation/federation.md) — `federation` is the engine's facade over the canonical `mergeSdl` function from `@suaveplan/api-graphql`. Apollo doesn'…
- [guards](./src/guards/depth-and-complexity.md) — `depth-and-complexity` walks a parsed `DocumentNode` and computes two coarse static metrics — the maximum selection-set…
- [depth-and-complexity](./src/guards/depth-and-complexity.md) — `depth-and-complexity` walks a parsed `DocumentNode` and computes two coarse static metrics — the maximum selection-set…
- [instruments](./src/instruments/instruments.md) — `instruments` owns the five OTel instrument names emitted by every GraphQL engine adapter in the foundation. Both `api-…
- [instruments](./src/instruments/instruments.md) — `instruments` owns the five OTel instrument names emitted by every GraphQL engine adapter in the foundation. Both `api-…
- [loaders](./src/loaders/loaders.md) — `loaders` defines the per-request DataLoader scope the engine creates around every incoming GraphQL operation. Two conc…
- [loaders](./src/loaders/loaders.md) — `loaders` defines the per-request DataLoader scope the engine creates around every incoming GraphQL operation. Two conc…
- [options](./src/options/options.md) — `options` owns the Zod-validated configuration surface accepted by `createApolloGraphqlEngine`. The schema mirrors the…
- [options](./src/options/options.md) — `options` owns the Zod-validated configuration surface accepted by `createApolloGraphqlEngine`. The schema mirrors the…
- [persisted-queries](./src/persisted-queries/persisted-queries.md) — `persisted-queries` implements Apollo's persisted-query manifest lookup — the ability for clients to send `{ id }` in p…
- [persisted-queries](./src/persisted-queries/persisted-queries.md) — `persisted-queries` implements Apollo's persisted-query manifest lookup — the ability for clients to send `{ id }` in p…
- [studio-reporting](./src/studio-reporting/studio-reporting.md) — `studio-reporting` materialises the configuration descriptor consumed by the engine when an operator opts in to Apollo…
- [studio-reporting](./src/studio-reporting/studio-reporting.md) — `studio-reporting` materialises the configuration descriptor consumed by the engine when an operator opts in to Apollo…
- [subscriptions](./src/subscriptions/subscriptions.md) — `subscriptions` runs a GraphQL subscription document against a wired schema, returning the `AsyncIterable<ExecutionResu…
- [subscriptions](./src/subscriptions/subscriptions.md) — `subscriptions` runs a GraphQL subscription document against a wired schema, returning the `AsyncIterable<ExecutionResu…
- [testing](./src/testing/conformance.md) — `testing/conformance` is the re-export of `runGraphqlEngineConformance` from `@suaveplan/api-graphql/testing`, pre-boun…
- [conformance](./src/testing/conformance.md) — `testing/conformance` is the re-export of `runGraphqlEngineConformance` from `@suaveplan/api-graphql/testing`, pre-boun…
<!-- end:auto-docs -->
## When to choose Apollo over Yoga
- You want Apollo Studio usage reporting (`studioReporting` option).
- You ship persisted-query manifests built by Apollo's CLI / Rover.
- Your team already runs Apollo's federation tooling in adjacent
services.
- You need Apollo's plugin ecosystem (cost analyser, request
coalescer, custom cache plugin, …).
Otherwise, Yoga is the faster default — fewer allocations per request,
slimmer dependency closure.
## Modules
- `engine` — `ApolloGraphqlEngine`, `createApolloGraphqlEngine`.
- `options` — Zod-validated `ApolloGraphqlOptions` schema.
- `error-codes` — `GRAPHQL_APOLLO.*` registry + `GraphqlApolloError`.
- `instruments` — engine-agnostic `graphql.*` OTel surface.
- `guards` — depth + complexity rejection helpers.
- `persisted-queries` — APQ manifest loader.
- `studio-reporting` — Apollo Studio descriptor bridge.
- `subscriptions` — `graphql-ws` runner.
- `loaders` — per-request DataLoader scope.
- `federation` — `mergeSdl` passthrough.
- `endpoints` — HTTP + UI proxy mounts.
- `testing` — `runApolloConformance` re-export.
## Status
Original. Owned by the `api-stack` super-category.
Passes the abstract conformance harness with the same invariants Yoga
satisfies — see `src/engine/apollo-engine.spec-conformance.test.ts`.

View File

@@ -0,0 +1,80 @@
# `endpoints/http` — `POST /graphql` + `GET /graphql/sdl`
## Purpose
`endpoints/http` mounts the two HTTP endpoints every GraphQL adapter
must expose: `POST /graphql` for operation dispatch and
`GET /graphql/sdl` for schema introspection by external tooling. The
paths are driven by `ApolloGraphqlOptions` so operators can rename
them — useful for shops that namespace internal APIs under `/internal/`.
The module is framework-agnostic in that it takes a `Router` from
`@suaveplan/api-router` and registers handlers via the router's verb
methods. Concrete router adapters (`api-router-hono`,
`api-router-in-memory`) consume the same `Router` interface.
## Features
- Mount paths driven by `ApolloGraphqlOptions.endpoint` /
`sdlEndpoint`.
- `executeQuery` handler accepts a `{ body, headers, context }` envelope
— the host's request pipeline decodes the HTTP frame into this shape
before dispatch.
- `sdl` handler returns the merged SDL synchronously.
- Returns a teardown function (currently no-op; reserved for hot
reload).
- Empty `headers` default — handlers that the host invokes without an
explicit map see `{}` rather than `undefined`.
## Usage: Basic
```ts
import { Router } from "@suaveplan/api-router";
import { mountGraphqlHttpEndpoints } from "@suaveplan/api-graphql-apollo/endpoints";
import { parseApolloGraphqlOptions } from "@suaveplan/api-graphql-apollo/options";
const router = new Router();
mountGraphqlHttpEndpoints({
router,
options: parseApolloGraphqlOptions(),
executeQuery: async (r) => ({ data: { ok: true } }),
sdl: () => "type Query { ok: Boolean! }",
});
```
## Usage: Advanced
Drive the executeQuery handler through the engine instead of a stub
so the router calls into Apollo Server's lifecycle on every POST:
```ts
import { createApolloGraphqlEngine } from "@suaveplan/api-graphql-apollo";
const engine = createApolloGraphqlEngine();
engine.buildSchema(registry);
mountGraphqlHttpEndpoints({
router,
options,
executeQuery: async ({ body, context }) => {
const req = body as { query: string; variables?: Record<string, unknown> };
return await engine.executeQuery(req, context as RequestContext);
},
sdl: () => engine.sdl() ?? "",
});
```
## API Reference
- `GraphqlHttpHandler` — `(req: { body, headers, context }) → Promise<unknown>`.
- `GraphqlSdlHandler` — `() → string`.
- `MountHttpEndpointsArgs` — `{ router, options, executeQuery, sdl }`.
- `mountGraphqlHttpEndpoints(args): () → void` — returns teardown.
## Implementation Notes
The handler returns a free-form `Record<string, unknown>` because the
host's transport adapter serialises the response to wire form. The
SDL handler is synchronous on purpose; SDL is computed once at
`buildSchema` time and held in memory thereafter. The teardown
closure is a no-op today but exists so callers do not need to rewrite
their host-wiring code when hot reload lands.

View File

@@ -0,0 +1,74 @@
# `endpoints/ui-proxy` — `/graphql/ui` proxy
## Purpose
`endpoints/ui-proxy` mounts the `/graphql/ui` endpoint. The Apollo
engine never embeds Apollo's landing page — UI is opt-in via a
separate package (GraphiQL, Altair, …), identical to Yoga's stance.
When no UI plugin is wired the endpoint returns `404` with a short
text body explaining the policy; when a plugin is wired the endpoint
forwards its `render()` output verbatim.
This module is deliberately framework-agnostic: it accepts a
`@suaveplan/api-router` `Router` and a UI plugin descriptor with a
`render()` method. The host wires whichever GraphiQL / Altair package
it prefers without the engine becoming aware of the choice.
## Features
- 404 by default — no Apollo landing page leakage.
- Custom mime type — UI plugins that ship binary (e.g. WASM) bodies
can override the default `text/html`.
- Driven by `ApolloGraphqlOptions.uiEndpoint` — operator can rename
the path.
- Returns a teardown handle reserved for future hot reload.
## Usage: Basic
```ts
import { Router } from "@suaveplan/api-router";
import { mountGraphqlUiProxy } from "@suaveplan/api-graphql-apollo/endpoints";
import { parseApolloGraphqlOptions } from "@suaveplan/api-graphql-apollo/options";
const router = new Router();
mountGraphqlUiProxy({
router,
options: parseApolloGraphqlOptions(),
ui: null,
});
// GET /graphql/ui → 404
```
## Usage: Advanced
Wire a GraphiQL-style UI plugin so the same router serves the
playground at `/graphql/ui`:
```ts
mountGraphqlUiProxy({
router,
options,
ui: {
render: () =>
`<!doctype html>
<html><body>
<div id="root"></div>
<script>/* GraphiQL bundle */</script>
</body></html>`,
},
});
```
## API Reference
- `GraphqlUiPlugin` — `{ render(): string; contentType?: string }`.
- `UiProxyResponse` — `{ status: 200 | 404; body: string; contentType: string }`.
- `mountGraphqlUiProxy(args): () → void`.
## Implementation Notes
The UI plugin contract is intentionally minimal — `render()` returns
a string and the proxy serves it. Future evolution (CSP nonces,
per-tenant configuration) lives in the UI plugin itself, not in this
module. The 404 response body is `text/plain` so curl users see a
human-readable explanation rather than an empty page.

View File

@@ -0,0 +1,105 @@
# `engine/apollo-engine` — `ApolloGraphqlEngine`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ApolloGraphqlEngine` is the concrete Apollo Server implementation of
the `@suaveplan/api-graphql` engine contract. It owns the Apollo
Server instance, the merged executable schema, the persisted-query
manifest plumbing, the request-time depth and complexity guards, and
the OTel instrument emission. The factory `createApolloGraphqlEngine`
hands out fresh instances; the class is `new`-able for tests and
power users that want subclass overrides.
Every operation pivots on three lifecycle calls: `buildSchema` once
at boot (or on hot reload), `executeQuery` per request, `dispose` at
shutdown. Schema rebuild gracefully tears down the prior Apollo
instance only when its startup has completed — a half-started server
is left alone, which is safe because the new server replaces the
slot.
## Features
- Implements every `GraphqlEngine` method — `buildSchema`,
`executeQuery`, `executeSubscription`, `dispose`.
- Pluggable `instruments`, `persistedQueries`, `apolloPlugins`
dependencies — sane defaults, full override.
- Depth + complexity guards run BEFORE Apollo sees the document so
the engine rejects pathological queries cheaply.
- `requestTimeoutMs` enforced via a race against a `setTimeout`-
driven rejection — the timer is `unref`'d so it never holds the
event loop open.
- `attachResolvers` resolves SDL-declared fields against the
registry's resolver maps; unknown types and unknown fields are
silently skipped so plugin authors can register superset maps
without runtime breakage.
- Introspection denial layered above Apollo's
`introspection: false` flag so the engine raises a typed error
before Apollo even validates the document.
## Usage: Basic
```ts
import { createGraphqlRegistry } from "@suaveplan/api-graphql";
import { createApolloGraphqlEngine } from "@suaveplan/api-graphql-apollo";
const engine = createApolloGraphqlEngine();
const registry = createGraphqlRegistry();
registry.addTypes("type Query { ping: String! }");
registry.addResolvers({ Query: { ping: () => "pong" } });
engine.buildSchema(registry);
const result = await engine.executeQuery(
{ query: "{ ping }" },
requestContext
);
await engine.dispose();
```
## Usage: Advanced
Wire persisted queries, custom instruments, a 5-second timeout, and a
custom Apollo plugin for cache control:
```ts
import { createApolloGraphqlEngine } from "@suaveplan/api-graphql-apollo";
import { createInstruments } from "@suaveplan/api-graphql-apollo/instruments";
import { createPersistedQueryManifest } from "@suaveplan/api-graphql-apollo/persisted-queries";
const instruments = createInstruments();
const persistedQueries = createPersistedQueryManifest([
["abc123", "{ ping }"],
]);
const engine = createApolloGraphqlEngine({
instruments,
persistedQueries,
apolloPlugins: [cacheControlPlugin],
options: { requestTimeoutMs: 5000, maxQueryDepth: 12 },
});
```
## API Reference
- `class ApolloGraphqlEngine<Caps> implements GraphqlEngine<Caps>`.
- `constructor(args?: CreateApolloGraphqlEngineArgs)`
- `buildSchema(registry): MergedSchema`
- `executeQuery(req, ctx): Promise<GraphqlResponse>`
- `executeSubscription(req, ctx): AsyncIterable<GraphqlResponse>`
- `dispose(): Promise<void>`
- `sdl(): string | null`
- `createApolloGraphqlEngine(args?): ApolloGraphqlEngine`.
- `raceWithTimeout<T>(p, ms): Promise<T>` — exported for unit testing.
## Implementation Notes
`startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests`
was rejected in favour of an explicit `start()` Promise because the
former turns a startup failure into per-request rejections we cannot
discriminate cleanly. The current implementation awaits the start
promise inside `executeQuery` and again inside `dispose` so a failed
start never leaves the engine in a half-stopped state. The
`raceWithTimeout` helper is exported so a focused unit test can
exercise both the resolve and reject branches without standing up an
Apollo Server.

View File

@@ -0,0 +1,86 @@
# `error-codes` — `GRAPHQL_APOLLO.*` registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`error-codes` defines the stable `GRAPHQL_APOLLO.*` namespace plus the
`GraphqlApolloError` class consumed by every other module in the
`@suaveplan/api-graphql-apollo` package. It sits in the same shape as the
sibling `@suaveplan/api-graphql` registry so callers can write a single
`instanceof SuaveplanError` discriminator and narrow further on
`error.code`.
The registry covers ten codes — request-document parse and validation
failures, depth and complexity guard rejections, schema-build failures
inside Apollo, the missing-transport case for subscriptions, request
timeouts, introspection-denied responses, persisted-query lookup misses,
and the resolver-attach catch-all. Every code is thrown by a production
path; the conformance harness enforces that empirically.
## Features
- Frozen `defineCodes("GRAPHQL_APOLLO", […])` registry — no untyped string
ever escapes the package.
- `GraphqlApolloError` exposes the code via both the inherited `error.code`
and a narrowed `graphqlApolloCode` field; either narrows downstream code
to the union of ten possible values.
- Constructor preserves `cause` and `context` per the `SuaveplanError`
contract — wrappers can attach the raw `cause: Error` for crash dumps.
- The class sets `this.name = "GraphqlApolloError"` so logs surface the
concrete subclass.
## Usage: Basic
```ts
import {
GRAPHQL_APOLLO_CODES,
GraphqlApolloError,
} from "@suaveplan/api-graphql-apollo/error-codes";
throw new GraphqlApolloError(
GRAPHQL_APOLLO_CODES.PARSE_FAILED,
"request document failed to parse"
);
```
## Usage: Advanced
Attach a cause and a context payload that the OTel span recorder will
attach as event attributes:
```ts
import { GraphqlApolloError } from "@suaveplan/api-graphql-apollo/error-codes";
try {
JSON.parse(maybeJson);
} catch (cause) {
throw new GraphqlApolloError(
"GRAPHQL_APOLLO.PARSE_FAILED",
"persisted-query manifest is not valid JSON",
{ cause: cause as Error, context: { path: "./pq.json" } }
);
}
```
## API Reference
- `GRAPHQL_APOLLO_CODES` — frozen object mapping each short name to its
fully-qualified code string.
- `GraphqlApolloErrorCode` — union of every code value resolved through
`GRAPHQL_APOLLO_CODES`.
- `class GraphqlApolloError extends SuaveplanError<GraphqlApolloErrorCode>`.
- `constructor(code, message, options?)`
- `graphqlApolloCode` — convenience accessor pre-narrowed to the
`GraphqlApolloErrorCode` union.
## Implementation Notes
The codes intentionally mirror the design-doc §10.5 list. Adding a new
code requires (a) appending it to the `defineCodes` array, (b) wiring at
least one production throw site, and (c) extending the conformance
checklist. The `name` field is set so structured logs see
`GraphqlApolloError` rather than the generic `SuaveplanError`. The class
extends the abstract `SuaveplanError` directly — no intermediate base.

View File

@@ -0,0 +1,70 @@
# `federation` — multi-plugin SDL merger
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`federation` is the engine's facade over the canonical `mergeSdl`
function from `@suaveplan/api-graphql`. Apollo doesn't see federated
plugin contributions directly — the engine collects every plugin's
SDL via the abstract `GraphqlRegistry`, federates them through this
module, and feeds the merged SDL to `buildSchema()`. Collision
detection, namespace wrapping, and strict-mode policy come for free
because all three are owned by the abstract merger.
The module is intentionally a one-line passthrough; it exists so the
package surface signals "yes, Apollo federates the same way Yoga
does — `GRAPHQL.SCHEMA_COLLISION` is the canonical code on either
engine".
## Features
- 1:1 delegation to `@suaveplan/api-graphql.mergeSdl` — no shape
change, no behaviour change.
- Same collision semantics as Yoga — `GRAPHQL.SCHEMA_COLLISION` is
raised, NOT a `GRAPHQL_APOLLO.*` code.
- Strict-mode propagation — `strictNamespace: true` requires every
plugin to declare a `graphql.namespace`.
- Returns the canonical `MergedSchema` shape so engine internals stay
assignable to the abstract contract.
## Usage: Basic
```ts
import { federateForApollo } from "@suaveplan/api-graphql-apollo/federation";
const merged = federateForApollo([
{ plugin: "billing", typeDefs: ["type Query { invoice: String! }"] },
{ plugin: "users", typeDefs: ["extend type Query { me: String! }"] },
]);
console.log(merged.sdl);
```
## Usage: Advanced
Enable strict-mode federation across all plugins to enforce a
namespace contract before the host even compiles:
```ts
import { federateForApollo } from "@suaveplan/api-graphql-apollo/federation";
federateForApollo(contributions, { strictNamespace: true });
// → throws GRAPHQL.NAMESPACE_REQUIRED if any plugin omitted namespace
```
## API Reference
- `federateForApollo(contributions, options?): MergedSchema`
- delegates to `@suaveplan/api-graphql.mergeSdl`.
## Implementation Notes
The function exists primarily so the package's TypeScript exports
expose a `federateForApollo` symbol that mirrors the sibling
`federateForYoga` (when the Yoga adapter lands). The behavioural
contract is owned by the abstract `mergeSdl`. Tests verify that
`GRAPHQL.SCHEMA_COLLISION` and `GRAPHQL.NAMESPACE_REQUIRED` are raised
through this wrapper — concrete proof that Apollo and Yoga agree on
the federation contract.

View File

@@ -0,0 +1,75 @@
# `guards/depth-and-complexity` — query-shape rejection
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`depth-and-complexity` walks a parsed `DocumentNode` and computes two
coarse static metrics — the maximum selection-set nesting depth and the
total field-count cost. The engine consults both before handing the
document to Apollo Server; queries that exceed `maxQueryDepth` or
`maxQueryComplexity` are rejected with the corresponding
`GRAPHQL_APOLLO.DEPTH_EXCEEDED` / `COMPLEXITY_EXCEEDED` codes.
The walk follows fragment spreads through their definitions and includes
inline-fragment subselections, so a clever query that hides depth or
cost behind a deeply nested fragment cannot bypass the guards. The
heuristic matches the Yoga sibling so the two engines reject the same
query set under identical limits.
## Features
- Depth counter increments per nested `selectionSet`, never on fragment
spreads or inline fragments themselves (matches conventional
depth-limiter implementations).
- Complexity counter adds 1 per selected field, plus the recursive cost
of its subselection, plus the expansion of fragment spreads. Missing
fragment spreads contribute zero.
- `assertDepthAndComplexity` evaluates depth first; the more localised
failure wins so operators see the rule they triggered.
- Pure-functional — no instance state, no `this`. Safe to call from any
request lifecycle phase.
## Usage: Basic
```ts
import { parse } from "graphql";
import { assertDepthAndComplexity } from "@suaveplan/api-graphql-apollo/guards";
const doc = parse("{ a { b { c } } }");
assertDepthAndComplexity(doc, { maxDepth: 10, maxComplexity: 1000 });
```
## Usage: Advanced
Run the metrics in isolation to feed an OTel histogram before the engine
even routes the document:
```ts
import { parse } from "graphql";
import {
maxQueryDepth,
queryComplexity,
} from "@suaveplan/api-graphql-apollo/guards";
const doc = parse(req.body.query);
metrics.histogram("graphql.depth").record(maxQueryDepth(doc));
metrics.histogram("graphql.complexity").record(queryComplexity(doc));
```
## API Reference
- `maxQueryDepth(doc: DocumentNode): number`
- `queryComplexity(doc: DocumentNode): number`
- `assertDepthAndComplexity(doc, { maxDepth, maxComplexity }): void` —
throws `GraphqlApolloError` on first violation.
## Implementation Notes
Fragment definitions are collected up-front into a `Map` so spread
resolution is O(1). Inline fragments share the parent depth — they do
not add a level. The complexity heuristic intentionally undercounts
list multiplicity; production deployments wire a richer estimator via a
host-supplied `costExtension` (out of scope for the foundation engine).

View File

@@ -0,0 +1,76 @@
# `instruments` — engine-agnostic OTel surface
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`instruments` owns the five OTel instrument names emitted by every GraphQL
engine adapter in the foundation. Both `api-graphql-apollo` and
`api-graphql-yoga` MUST emit the same set so operator dashboards survive
an engine swap with zero rename work. The names are stable strings (not
generated identifiers); flipping the host's `engine` slot from Yoga to
Apollo or back keeps the time-series continuous.
The module also ships an in-memory `GraphqlInstruments` implementation
used by unit tests — `createInstruments()` returns a recorder that pushes
to plain arrays so tests can assert exact request counts, schema rebuild
counts, etc. Production hosts pass their own implementation backed by
`@opentelemetry/sdk-metrics`.
## Features
- `GRAPHQL_INSTRUMENT_NAMES` frozen object — five canonical
`graphql.*` names.
- `createInstruments()` — zero-arg factory returning an in-memory
recorder with `snapshot()` for assertions.
- Counter + histogram surface: `incRequests`, `recordRequestDurationMs`,
`recordResolverDurationMs`, `incSubscriptionsActive` /
`decSubscriptionsActive`, `incSchemaRebuilds`.
- Snapshot is a deep copy — tests can compare without worrying about
later mutation aliasing the saved value.
## Usage: Basic
```ts
import { createInstruments } from "@suaveplan/api-graphql-apollo/instruments";
const ins = createInstruments();
ins.incRequests({ operation: "ping" });
ins.recordRequestDurationMs(42);
ins.snapshot();
// → { requestsTotal: 1, requestDurationMs: [42], … }
```
## Usage: Advanced
Plug an OTel-backed instrument set into the engine and assert the
expected metrics are emitted across a workload:
```ts
import { createApolloGraphqlEngine } from "@suaveplan/api-graphql-apollo";
import { createInstruments } from "@suaveplan/api-graphql-apollo/instruments";
const ins = createInstruments();
const engine = createApolloGraphqlEngine({ instruments: ins });
// … buildSchema + executeQuery …
const snap = ins.snapshot();
console.log(snap.requestsTotal, snap.schemaRebuildsTotal);
```
## API Reference
- `GRAPHQL_INSTRUMENT_NAMES.requestsTotal` (etc.) — exact metric names.
- `GraphqlInstruments` — interface implemented by the in-memory recorder
and by production OTel adapters.
- `InstrumentSnapshot` — read-only snapshot returned by `snapshot()`.
- `createInstruments(): GraphqlInstruments` — in-memory factory.
## Implementation Notes
The recorder is deliberately a closure over four primitives + two arrays
— no Map allocations on the hot path. The arrays are copied at snapshot
time so callers cannot mutate the live counters. Engine-agnostic naming
is enforced by the engine-swap integration test in `tests/integration/`
which asserts the metric-name set is identical across Apollo and Yoga.

View File

@@ -0,0 +1,75 @@
# `loaders` — per-request DataLoader scope
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`loaders` defines the per-request DataLoader scope the engine creates
around every incoming GraphQL operation. Two concurrent requests
receive distinct loader instances; the cache never leaks across
request boundaries even though both requests draw from the same set of
loader factories registered on the abstract `GraphqlRegistry`.
The module is deliberately the thinnest possible wrapper around the
`LoaderFactory.create()` surface owned by `@suaveplan/api-types/graphql`.
The engine calls `createRequestLoaderScope()` once per request; resolvers
that need a loader call `scope.get(name, factory)`; the engine disposes
the scope at request end.
## Features
- One scope per request — distinct cache per request guaranteed.
- Loader lazily instantiated on first `get(name, factory)` call;
subsequent calls reuse the instance.
- `dispose()` blocks subsequent `get()` calls so a buggy late resolver
cannot resurrect a freed cache.
- Zero runtime dependencies — pure JS, fits in ~40 lines.
## Usage: Basic
```ts
import { createRequestLoaderScope } from "@suaveplan/api-graphql-apollo/loaders";
const scope = createRequestLoaderScope();
const userLoader = scope.get("users", userLoaderFactory);
const user = await userLoader.load("u_42");
scope.dispose();
```
## Usage: Advanced
Wire the scope into a resolver context object so resolvers consume
loaders by name through the request context:
```ts
import { createRequestLoaderScope } from "@suaveplan/api-graphql-apollo/loaders";
function makeContext(): RequestContext {
const scope = createRequestLoaderScope();
return {
loaders: {
users: () => scope.get("users", userLoaderFactory),
orders: () => scope.get("orders", orderLoaderFactory),
},
finalize: () => scope.dispose(),
} as unknown as RequestContext;
}
```
## API Reference
- `RequestLoaderScope`:
- `get<TKey, TValue>(name, factory): { load }`
- `dispose(): void`
- `size(): number`
- `createRequestLoaderScope(): RequestLoaderScope`.
## Implementation Notes
The scope is a closure over a `Map<string, LoaderInstance>` plus a
`disposed` boolean. Throwing on `get` after `dispose` surfaces the
late-resolver bug at the call site instead of silently producing
inconsistent reads. The size accessor is provided so OTel histograms
can record loader-fanout per request.

View File

@@ -0,0 +1,83 @@
# `options` — `ApolloGraphqlOptions`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`options` owns the Zod-validated configuration surface accepted by
`createApolloGraphqlEngine`. The schema mirrors the Yoga sibling's options
1:1 so an operator can swap engines without touching the host wiring —
defaults agree, paths agree, depth and complexity limits agree, and the
persisted-query path / Studio reporting fields are the Apollo-specific
additions layered on top.
Inputs cross the Zod boundary once at engine construction time; downstream
code consumes the fully resolved `ApolloGraphqlOptions` shape and never
re-validates. Configuration failures throw `GRAPHQL_APOLLO.PARSE_FAILED`
so caller-visible input errors share a single error code with the
request-document parse path.
## Features
- Endpoint paths default to `/graphql`, `/graphql/sdl`, `/graphql/ws`,
`/graphql/ui` — identical to Yoga.
- `introspection: false` by default; flipping the flag wires Apollo's
schema introspection on AND lets the engine skip the
`INTROSPECTION_DENIED` guard.
- `maxQueryDepth` and `maxQueryComplexity` mirror the Yoga defaults
(`10`, `1000`).
- `requestTimeoutMs` defaults to `30_000`; the engine wraps every
`executeOperation` call in a timeout race.
- Persisted-query manifest path is optional; the engine wires the
manifest loader only when `persistedQueriesPath` is set.
- `studioReporting` opt-in — the engine returns `null` from
`createStudioReporting()` when omitted.
## Usage: Basic
```ts
import { parseApolloGraphqlOptions } from "@suaveplan/api-graphql-apollo/options";
const opts = parseApolloGraphqlOptions({});
// opts.endpoint === "/graphql"
```
## Usage: Advanced
Wire persisted queries plus Apollo Studio reporting plus a custom error
formatter:
```ts
import { parseApolloGraphqlOptions } from "@suaveplan/api-graphql-apollo/options";
const opts = parseApolloGraphqlOptions({
persistedQueriesPath: "./pq.json",
studioReporting: {
apiKey: process.env.APOLLO_KEY ?? "",
graphRef: "supergraph@current",
},
formatError: (err) => ({ msg: String(err), redacted: true }),
requestTimeoutMs: 5_000,
});
```
## API Reference
- `apolloGraphqlOptionsSchema` — the underlying Zod schema (exported for
introspection / OpenAPI registration).
- `parseApolloGraphqlOptions(input?) → ApolloGraphqlOptions` — throws
`GraphqlApolloError("GRAPHQL_APOLLO.PARSE_FAILED")` on validation
failure.
- `ApolloGraphqlOptions` — fully-resolved options type (every field
required, every default applied).
- `ApolloGraphqlOptionsInput` — input type (every field optional).
## Implementation Notes
The schema uses `z.string().startsWith("/")` for every endpoint path —
operator typos like `endpoint: "graphql"` (no leading slash) surface
synchronously at boot. The `formatError` field is `z.custom<(err) => …>`
because Zod has no first-class function type; the custom guard rejects
non-functions with the same `GRAPHQL_APOLLO.PARSE_FAILED` code.

View File

@@ -0,0 +1,74 @@
# `persisted-queries` — APQ manifest support
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`persisted-queries` implements Apollo's persisted-query manifest lookup
— the ability for clients to send `{ id }` in place of `{ query }` and
have the engine resolve the id against a build-time-generated manifest.
The feature is Apollo-specific (Yoga uses a different protocol variant)
so the loader lives in this adapter and not in the abstract
`api-graphql` core.
The module exposes three entry points: a filesystem loader, an
in-memory parser for callers that already have the JSON text, and a
direct-from-entries factory for tests. Lookups against an unknown id
raise `GRAPHQL_APOLLO.PERSISTED_QUERY_NOT_FOUND`; malformed input
raises `GRAPHQL_APOLLO.PARSE_FAILED` so callers see a single
configuration-failure code for any manifest shape error.
## Features
- File loader uses `node:fs/promises.readFile` — async-safe in the host
start-up sequence.
- JSON parser validates the manifest is an object of `string → string`
before returning; non-string values raise `PARSE_FAILED`.
- Direct factory accepts any `Iterable<[id, query]>` — useful in tests.
- Manifest is frozen on construction; the only mutation surface is
`lookup(id)` (reads) and `has(id)` (existence check).
- `entries` accessor returns the frozen map for callers that need to
diff manifests across deploys.
## Usage: Basic
```ts
import { loadPersistedQueryManifest } from "@suaveplan/api-graphql-apollo/persisted-queries";
const manifest = await loadPersistedQueryManifest("./pq.json");
const query = manifest.lookup("abc123");
```
## Usage: Advanced
Build a manifest from an in-process source (e.g. a KV-store fetch) and
wire it into the engine for runtime lookup:
```ts
import { createApolloGraphqlEngine } from "@suaveplan/api-graphql-apollo";
import { createPersistedQueryManifest } from "@suaveplan/api-graphql-apollo/persisted-queries";
const entries = await kv.list("pq:");
const manifest = createPersistedQueryManifest(
entries.map((e) => [e.key.slice(3), e.value as string])
);
const engine = createApolloGraphqlEngine({ persistedQueries: manifest });
```
## API Reference
- `PersistedQueryManifest` — `{ entries, lookup, has }`.
- `loadPersistedQueryManifest(path: string): Promise<PersistedQueryManifest>`
- `parsePersistedQueryManifest(raw: string): PersistedQueryManifest`
- `createPersistedQueryManifest(entries): PersistedQueryManifest`
## Implementation Notes
Manifest entries are stored in a `Map<string, string>` — O(1) lookup,
deterministic iteration order. The `entries` field returns a new `Map`
constructed from the underlying entries on every access so callers
cannot mutate the live state. The loader maps every filesystem and JSON
failure to `GRAPHQL_APOLLO.PARSE_FAILED` so wire-level diagnostics show
a single code regardless of failure mode.

View File

@@ -0,0 +1,78 @@
# `studio-reporting` — Apollo Studio bridge
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`studio-reporting` materialises the configuration descriptor consumed
by the engine when an operator opts in to Apollo Studio usage
reporting. The module is deliberately small: it validates the supplied
`apiKey` / `graphRef` pair, returns a frozen descriptor, and lets the
engine forward the descriptor to Apollo's `ApolloServerPluginUsageReporting`
without leaking the SDK weight into the dependency closure of shops
that do not use Studio.
When the operator does not supply a configuration, the factory returns
`null` and the engine instantiates Apollo Server without the reporting
plugin — the request hot path pays nothing for the unused feature.
## Features
- Optional configuration — the factory short-circuits to `null` when
the caller passes `undefined`.
- Frozen descriptor — the returned object is immutable so downstream
code cannot mutate the API-key surface.
- Pluggable — production callers can pre-emptively swap the descriptor
for a mock plugin in a staging environment.
- zero dependency on the Apollo Studio plugin package — the
bridge is data-only.
## Usage: Basic
```ts
import { createStudioReporting } from "@suaveplan/api-graphql-apollo/studio-reporting";
const d = createStudioReporting({
apiKey: process.env.APOLLO_KEY ?? "",
graphRef: "supergraph@current",
});
```
## Usage: Advanced
Conditionally enable reporting per environment without forking the
engine wire-up code:
```ts
import { createApolloGraphqlEngine } from "@suaveplan/api-graphql-apollo";
import { createStudioReporting } from "@suaveplan/api-graphql-apollo/studio-reporting";
const studio = createStudioReporting(
process.env.APOLLO_KEY === undefined
? undefined
: {
apiKey: process.env.APOLLO_KEY,
graphRef: "supergraph@" + (process.env.STAGE ?? "current"),
}
);
const engine = createApolloGraphqlEngine({
apolloPlugins: studio === null ? [] : [/* wire Studio plugin from descriptor */],
});
```
## API Reference
- `StudioReportingConfig` — `{ apiKey, graphRef }`.
- `StudioReportingDescriptor` — `{ enabled: true, apiKey, graphRef }`.
- `createStudioReporting(config | undefined) → StudioReportingDescriptor | null`.
## Implementation Notes
The descriptor is `Object.freeze`d so callers cannot mutate it after
construction. The factory is the only entry point — there is no
mutable runtime state. The actual Studio plugin is dynamically loaded
by callers that want to wire it; this avoids pulling
`@apollo/server-plugin-usage-reporting` into the dependency closure
when the feature is off.

View File

@@ -0,0 +1,81 @@
# `subscriptions` — `graphql-ws` bridge
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`subscriptions` runs a GraphQL subscription document against a wired
schema, returning the `AsyncIterable<ExecutionResult>` the engine
forwards over the host's `api-ws` transport. Subscriptions ride
`graphql-ws` semantically; this module is the framework-agnostic core
that the engine's transport adapter wraps.
When the host has not wired an `api-ws` engine, the
`assertSubscriptionTransport` guard raises
`GRAPHQL_APOLLO.SUBSCRIPTION_TRANSPORT_UNAVAILABLE` so that the missing
infrastructure surfaces at the first subscription frame — not later
when a publish silently never reaches a subscriber.
## Features
- Direct `graphql.subscribe()` delegation — no extra framing.
- Accepts both `DocumentNode` and SDL string for the operation.
- Wraps a single-result rejection in a one-shot AsyncIterable so the
caller's frame loop sees one error frame and closes cleanly.
- Transport guard raises a typed `GraphqlApolloError` with the
`SUBSCRIPTION_TRANSPORT_UNAVAILABLE` code.
- Validates the document contains a subscription operation — non-
subscription operations raise
`GRAPHQL_APOLLO.VALIDATION_FAILED`.
## Usage: Basic
```ts
import { runSubscription } from "@suaveplan/api-graphql-apollo/subscriptions";
const iter = await runSubscription({
schema,
document: "subscription { ticks }",
contextValue: { tenantId: "t_1" },
});
for await (const frame of iter) {
console.log(frame);
}
```
## Usage: Advanced
Pair the runtime guard with the host's `api-ws` wiring so subscription
frames are rejected at the wire boundary when the transport is not
configured:
```ts
import {
assertSubscriptionTransport,
runSubscription,
} from "@suaveplan/api-graphql-apollo/subscriptions";
const transport = host.adapters.ws ?? null;
assertSubscriptionTransport(transport);
const iter = await runSubscription({ schema, document, contextValue });
```
## API Reference
- `SubscriptionTransport` — `{ protocol: "graphql-ws" }`.
- `assertSubscriptionTransport(transport | null)` — throws
`GraphqlApolloError` on miss.
- `runSubscription({ schema, document, variableValues?,
operationName?, contextValue })`.
## Implementation Notes
When `graphql.subscribe()` returns a single `ExecutionResult` (e.g.
the subscribe field threw synchronously), the helper wraps it in a
single-iteration AsyncIterable so callers' frame loops handle the
error path identically to the streaming path. The validation step
sniffs the document for any `OPERATION_DEFINITION` of kind
`subscription`; documents that contain only query/mutation operations
reject with `VALIDATION_FAILED`.

View File

@@ -0,0 +1,62 @@
# `testing/conformance` — Apollo-bound conformance harness
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`testing/conformance` is the re-export of `runGraphqlEngineConformance`
from `@suaveplan/api-graphql/testing`, pre-bound to the Apollo factory.
Consumers call `runApolloConformance()` and the harness exercises the
abstract `GraphqlEngine` contract against the concrete Apollo adapter.
Both `api-graphql-apollo` and `api-graphql-yoga` MUST pass the same
invariants — operator behaviour is engine-agnostic at the contract
boundary.
The module also re-exports the unmodified `runGraphqlEngineConformance`
so downstream packages that want to run the harness against a
*different* factory (e.g. a federated subgraph composing Apollo + a
custom transport) keep a single import surface.
## Features
- One-line invocation — `runApolloConformance()` runs every scenario.
- Pre-bound factory — callers do not need to know Apollo's
construction args.
- Labelled — the second-positional `label` parameter shows up in
test output so split CI logs are unambiguous.
- Re-exports `runGraphqlEngineConformance` for advanced callers.
## Usage: Basic
```ts
import { runApolloConformance } from "@suaveplan/api-graphql-apollo/testing";
runApolloConformance();
```
## Usage: Advanced
Run the conformance suite under a custom label so two adapter variants
appear distinctly in CI output:
```ts
import { runApolloConformance } from "@suaveplan/api-graphql-apollo/testing";
runApolloConformance("apollo (persisted-queries)");
runApolloConformance("apollo (no-persisted-queries)");
```
## API Reference
- `runApolloConformance(label?: string): void`.
- `runGraphqlEngineConformance(opts): void` — unchanged re-export.
## Implementation Notes
The Apollo-bound factory yields a fresh engine per scenario so each
invariant exercises a clean lifecycle. The `makeRequestContext` slot
is the cast `{} as RequestContext` — sufficient for the abstract
contract because the harness never touches context fields beyond
hand-off. Tests that need a richer context construct one explicitly.