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.fieldcollisions and raisesGRAPHQL.SCHEMA_COLLISIONwithdetails.pathanddetails.plugins = [A, B]alphabetically sorted. - Applies the
graphql.namespacewrapper:Query/Mutation/Subscriptionextensions land under<Namespace>Queryetc., with the root operation type extended to expose the wrapper. - Strict mode (
{ strictNamespace: true }) raisesGRAPHQL.NAMESPACE_REQUIREDwhen any plugin omits a namespace. - Returns an immutable
MergedSchemawith the concatenated SDL, theType → 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.