Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View 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.