Initial Commit
This commit is contained in:
84
packages/api-stack/api/api-graphql/src/registry/registry.md
Normal file
84
packages/api-stack/api/api-graphql/src/registry/registry.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# `registry`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Concrete accumulator-style `GraphqlRegistry` implementation. The host hands
|
||||
each plugin one of these during the GraphQL registration phase (design
|
||||
§10.1); plugins call `addTypes` / `addResolvers` / `addLoader` to contribute
|
||||
their schema slice. At schema-build time the engine calls `freeze()` to
|
||||
obtain an immutable snapshot.
|
||||
|
||||
Generic over the capability slice (`Caps`) so resolvers and loader factories
|
||||
see the typed `RequestContext<Caps>` shape declared by
|
||||
`@suaveplan/api-types/plugin-context`.
|
||||
|
||||
## Features
|
||||
|
||||
- `addTypes` / `addResolvers` accumulate — multiple calls compose rather
|
||||
than overwrite.
|
||||
- `addLoader` rejects duplicate names with `GRAPHQL.LOADER_DUPLICATE_NAME`
|
||||
and wraps the supplied `BatchFn` in a `LoaderFactory` whose `create()`
|
||||
yields a DataLoader-like per-request handle.
|
||||
- `freeze()` returns an immutable snapshot and blocks further writes.
|
||||
- Optional `plugin` option threads the plugin name through error details
|
||||
so collisions are diagnostic-rich.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createGraphqlRegistry } from "@suaveplan/api-graphql/registry";
|
||||
|
||||
const registry = createGraphqlRegistry({ plugin: "Billing" });
|
||||
registry.addTypes("extend type Query { invoices: [Invoice!]! }");
|
||||
registry.addResolvers({ Query: { invoices: async () => [] } });
|
||||
registry.addLoader<string, Invoice>("invoiceById", async (ids) =>
|
||||
ids.map((id) => invoicesById.get(id) ?? null)
|
||||
);
|
||||
const snapshot = registry.freeze();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
The `LoaderFactory.create()` returns a handle that batches `load(key)` calls
|
||||
within a single microtask tick. Within one request the host calls
|
||||
`create()` once and stores the handle on `RequestContext.capabilities`:
|
||||
|
||||
```ts
|
||||
import { createGraphqlRegistry } from "@suaveplan/api-graphql/registry";
|
||||
|
||||
const registry = createGraphqlRegistry();
|
||||
registry.addLoader<string, User>("userById", async (ids) => {
|
||||
const rows = await db.from("users").whereIn("id", ids);
|
||||
return ids.map((id) => rows.find((r) => r.id === id) ?? null);
|
||||
});
|
||||
|
||||
const factory = registry.freeze().loaders.get("userById")!;
|
||||
const loader = factory.create();
|
||||
const [a, b, c] = await loader.loadMany(["u1", "u2", "u3"]);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createGraphqlRegistry<Caps>(options?) → GraphqlRegistry<Caps> & { freeze }` —
|
||||
build a fresh accumulator.
|
||||
- `GraphqlRegistry<Caps>` — `{ addTypes, addResolvers, addLoader }`.
|
||||
- `FrozenRegistry<Caps>` — `{ typeDefs, resolvers, loaders }`.
|
||||
- `CreateRegistryOptions` — `{ plugin? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The freeze pattern matches what the host needs: plugins write during their
|
||||
`onRegister` phase; engines read during `buildSchema`. Once frozen, any
|
||||
mutation raises `GRAPHQL.LOADER_DUPLICATE_NAME` (the message indicates the
|
||||
registry is frozen — the code is shared because a frozen mutation is the
|
||||
same family of error as a duplicate).
|
||||
|
||||
The bundled `LoaderFactory` uses microtask batching: `load(key)` enqueues
|
||||
and the queue flushes on `Promise.resolve().then(...)`. This matches the
|
||||
DataLoader semantics most engines expect. Engines that prefer their own
|
||||
loader runtime can ignore the bundled handle and call `factory.create()`
|
||||
once per request, then forward to their own DataLoader at the boundary.
|
||||
Reference in New Issue
Block a user