Files
suaveplan/packages/api-stack/api/api-graphql/src/registry/registry.md
2026-05-27 23:29:53 +01:00

3.2 KiB

registry

(no submodules)

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

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:

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.