Files
skills/docs/api-stack/api/api-graphql/src/federation/merge-sdl.md
2026-05-29 08:20:56 +01:00

3.5 KiB

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 <Namespace>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

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:

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.