Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# `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
|
||||
|
||||
```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.
|
||||
@@ -0,0 +1,73 @@
|
||||
# `namespace`
|
||||
|
||||
## Purpose
|
||||
|
||||
Apply the `graphql.namespace` wrapper to a plugin's parsed SDL blocks.
|
||||
Plugins that declare `namespace: "Billing"` in their manifest get their
|
||||
root-operation extensions repackaged: `extend type Query { invoices }`
|
||||
becomes `type BillingQuery { invoices }` plus
|
||||
`extend type Query { billing: BillingQuery! }`. Non-root types pass
|
||||
through unchanged.
|
||||
|
||||
This is the second half of the federation story — `mergeSdl` calls
|
||||
`applyNamespace` on each plugin's contributions before unioning them, so
|
||||
namespaced plugins never collide on root fields.
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps `Query`, `Mutation`, `Subscription` extensions in
|
||||
`<Ns>Query`/`<Ns>Mutation`/`<Ns>Subscription` types.
|
||||
- Emits exactly one root-extension per wrapped operation, even when the
|
||||
plugin contributed multiple `extend type Query` blocks.
|
||||
- Lower-cases the first character of the namespace label so it works as
|
||||
a field name (`Billing` → `billing`).
|
||||
- Leaves non-root types unchanged — entity types like `Invoice` stay at
|
||||
the top level.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { applyNamespace, parseSdl } from "@suaveplan/api-graphql/federation";
|
||||
|
||||
const blocks = parseSdl("extend type Query { invoices: [Invoice!]! }");
|
||||
const wrapped = applyNamespace(blocks, "Billing");
|
||||
// wrapped[0] is `type BillingQuery { invoices: [Invoice!]! }`
|
||||
// wrapped[1] is `extend type Query { billing: BillingQuery! }`
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Multiple root extensions in one plugin collapse to one wrapper root field:
|
||||
|
||||
```ts
|
||||
import { applyNamespace, parseSdl } from "@suaveplan/api-graphql/federation";
|
||||
|
||||
const blocks = parseSdl(
|
||||
"extend type Query { a: Int! } extend type Query { b: Int! }"
|
||||
);
|
||||
const wrapped = applyNamespace(blocks, "Demo");
|
||||
// Two `type DemoQuery` blocks (the engine merges them) but only one
|
||||
// `extend type Query { demo: DemoQuery! }` is emitted.
|
||||
```
|
||||
|
||||
The merger consolidates multiple `type DemoQuery` blocks via the field-list
|
||||
deduplication path; the single root-extension keeps `Query` clean.
|
||||
|
||||
## API Reference
|
||||
|
||||
- `applyNamespace(blocks, namespace) → TypeBlock[]` — wrap root-operation
|
||||
blocks; pass non-root blocks through.
|
||||
- `namespaceFieldName(namespace) → string` — `"Billing"` → `"billing"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The wrapping is purely textual at the block level — the inner block has
|
||||
its `extend type Query` prefix rewritten to `type BillingQuery` and the
|
||||
generated root-extension is a short SDL string. The merger's collision
|
||||
detector still runs over the wrapped output, so two plugins that both
|
||||
declare `namespace: "Billing"` will collide on `Query.billing` and raise
|
||||
`GRAPHQL.SCHEMA_COLLISION` as expected.
|
||||
|
||||
Empty namespace strings short-circuit: the input blocks are returned
|
||||
verbatim. Callers MUST treat `namespace === ""` and `namespace === undefined`
|
||||
identically — the federation entry point already does so.
|
||||
@@ -0,0 +1,73 @@
|
||||
# `parse-sdl`
|
||||
|
||||
## Purpose
|
||||
|
||||
Minimal SDL parser used by the federation merger to locate `type Name { ... }`
|
||||
and `extend type Name { ... }` blocks plus the field names inside each.
|
||||
This is **not** a full GraphQL AST parser — it is the contract-layer
|
||||
dependency-free helper that recognises the constructs the federation rules
|
||||
care about (design §10.2).
|
||||
|
||||
Engine adapters re-parse the merged SDL with their own AST machinery. The
|
||||
purpose of this parser is the cross-plugin invariant check before the
|
||||
engine ever sees the schema.
|
||||
|
||||
## Features
|
||||
|
||||
- Recognises `type Name { ... }` and `extend type Name { ... }` blocks.
|
||||
- Extracts field names (handles inline argument lists `field(a: Int): T`).
|
||||
- Strips line comments (`# ...`) and block string descriptions
|
||||
(`"""..."""`) before parsing.
|
||||
- Ignores union / interface / input / enum / scalar declarations — these
|
||||
pass through to the merged SDL but don't contribute to the field-level
|
||||
collision check.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseSdl } from "@suaveplan/api-graphql/federation";
|
||||
|
||||
const blocks = parseSdl(
|
||||
"type Invoice { id: ID! total: Int! } extend type Query { invoices: [Invoice!]! }"
|
||||
);
|
||||
console.log(blocks[0].typeName); // "Invoice"
|
||||
console.log(blocks[1].kind); // "extend"
|
||||
console.log(blocks[1].fields); // ["invoices"]
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Strip descriptions before parsing for downstream tooling that wants only
|
||||
the structural SDL:
|
||||
|
||||
```ts
|
||||
import { stripCommentsAndDescriptions } from "@suaveplan/api-graphql/federation";
|
||||
|
||||
const cleaned = stripCommentsAndDescriptions(`
|
||||
"""Invoice domain root"""
|
||||
type Invoice {
|
||||
# the canonical id
|
||||
id: ID!
|
||||
}
|
||||
`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `parseSdl(sdl) → TypeBlock[]` — extract structured blocks.
|
||||
- `stripCommentsAndDescriptions(sdl) → string` — comment + block-string
|
||||
removal.
|
||||
- `TypeBlock` — `{ kind: "type" | "extend"; typeName; fields; rawBlock }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The parser uses three regexes: one for block-string descriptions, one for
|
||||
line comments, and one for the type/extend block envelope. Field-name
|
||||
extraction is line-oriented; multi-line argument lists fall back to the
|
||||
inline-argument regex, which is `([a-z_][a-z0-9_]*)\s*(?:\([^)]*\))?\s*:`.
|
||||
|
||||
Pathological inputs (nested braces inside field type expressions, e.g.
|
||||
`field: { ... }`) are not supported — GraphQL SDL doesn't use them. If a
|
||||
future GraphQL spec extension introduces nested braces, the regex would
|
||||
need replacing with a proper tokeniser; that's a known limit and acceptable
|
||||
for the contract layer.
|
||||
Reference in New Issue
Block a user