# `merge-sdl` ## Purpose Federate one or more plugin SDL contributions into a single merged schema. This is the engine-agnostic federation step described in design §10.2: each plugin hands the host a `SdlContribution`; the host calls `mergeSdl` to produce the canonical SDL string and field map an engine consumes when building its executable schema. The merger has no `graphql.js` dependency — it parses just enough SDL to detect field-level collisions and apply the `graphql.namespace` wrapper. Engines re-parse the result with their own AST machinery; the merger's role is the cross-plugin invariant check. ## Features - Unions `extend type Query { ... }` blocks from multiple plugins. - Detects `Type.field` collisions and raises `GRAPHQL.SCHEMA_COLLISION` with `details.path` and `details.plugins = [A, B]` alphabetically sorted. - Applies the `graphql.namespace` wrapper: `Query`/`Mutation`/`Subscription` extensions land under `Query` etc., with the root operation type extended to expose the wrapper. - Strict mode (`{ strictNamespace: true }`) raises `GRAPHQL.NAMESPACE_REQUIRED` when any plugin omits a namespace. - Returns an immutable `MergedSchema` with the concatenated SDL, the `Type → fields[]` map, and the alphabetically-sorted plugin list. ## Usage: Basic ```ts import { mergeSdl } from "@suaveplan/api-graphql/federation"; const merged = mergeSdl([ { plugin: "Billing", typeDefs: ["extend type Query { invoices: [Invoice!]! } type Invoice { id: ID! }"], }, { plugin: "Identity", typeDefs: ["extend type Query { users: [User!]! } type User { id: ID! }"], }, ]); console.log(merged.sdl); console.log(merged.typeFields.Query); // ["invoices", "users"] ``` ## Usage: Advanced Strict mode + namespace wrapping for a multi-tenant host where every plugin must be reachable under its own root field: ```ts import { mergeSdl, GraphqlError } from "@suaveplan/api-graphql"; try { mergeSdl( [ { plugin: "Billing", namespace: "Billing", typeDefs: ["extend type Query { invoices: [Invoice!]! }"], }, { plugin: "Identity", // no namespace — strict mode rejects typeDefs: ["extend type Query { users: [User!]! }"], }, ], { strictNamespace: true } ); } catch (e) { if (e instanceof GraphqlError) { console.error(e.graphqlCode); // "GRAPHQL.NAMESPACE_REQUIRED" } } ``` ## API Reference - `mergeSdl(contributions, options?) → MergedSchema` — federation entry point. - `SdlContribution` — `{ plugin, typeDefs, namespace? }`. - `MergedSchema` — `{ sdl, typeFields, plugins }`. - `MergeOptions` — `{ strictNamespace? }`. - `SchemaCollisionDetail` — error-detail shape for the collision code. ## Implementation Notes The collision detector tracks the first plugin that registered each `Type.field` and raises on the second non-self registration. Repeated declarations from the same plugin are tolerated (the federation step is re-runnable). Sorting the plugin pair before raising keeps error messages deterministic in tests. `extend` vs concrete `type` is recorded but not enforced by the merger itself — engines reject schemas with multiple concrete declarations of the same type at schema-build time. The merger's invariant is field-level: two plugins MAY both add fields to `Query` (via `extend`), but they MUST NOT add the same field name.