first commit
This commit is contained in:
69
docs/api-stack/api/api-graphql-apollo/README.md
Normal file
69
docs/api-stack/api/api-graphql-apollo/README.md
Normal 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`.
|
||||
|
||||
80
docs/api-stack/api/api-graphql-apollo/src/endpoints/http.md
Normal file
80
docs/api-stack/api/api-graphql-apollo/src/endpoints/http.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
75
docs/api-stack/api/api-graphql-apollo/src/loaders/loaders.md
Normal file
75
docs/api-stack/api/api-graphql-apollo/src/loaders/loaders.md
Normal 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.
|
||||
83
docs/api-stack/api/api-graphql-apollo/src/options/options.md
Normal file
83
docs/api-stack/api/api-graphql-apollo/src/options/options.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user