SubGraph plugin
Tag types and fields into named sub-graphs, then build a public or internal view of one schema.
The sub-graph plugin lets you tag types and fields with named sub-graphs, then ask the builder for just the slice you want. You write each type once; builder.toSchema({ subGraph }) returns a filtered GraphQLSchema with everything outside the named sub-graph removed. The common use is a public API that exposes a subset of a richer internal graph.
Install
npm install --save @pothos/plugin-sub-graphTagging and building a view
Register the plugin, declare your sub-graph names on the SubGraphs generic, then set defaults on the builder. defaultForTypes puts every type into those sub-graphs unless it says otherwise, and fieldsInheritFromTypes makes a field default to its parent type's membership. Individual fields and types opt out with their own subGraphs array.
const builder = new SchemaBuilder<{
SubGraphs: 'Public' | 'Internal';
}>({
plugins: [SubGraphPlugin],
subGraphs: {
// A type with no subGraphs of its own belongs to every sub-graph.
defaultForTypes: ['Public', 'Internal'],
// A field with no subGraphs inherits its parent type's membership.
fieldsInheritFromTypes: true,
},
});
// The whole Player type lives only in the Internal graph.
const Player = builder.objectRef<IPlayer>('Player').implement({
subGraphs: ['Internal'],
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
salary: t.exposeInt('salary'),
}),
});
const Team = builder.objectRef<ITeam>('Team').implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
// A single field held back from the Public graph.
budget: t.exposeInt('budget', { subGraphs: ['Internal'] }),
// Returns an Internal-only type, so it can only live in Internal.
roster: t.field({
type: [Player],
subGraphs: ['Internal'],
resolve: (team) => team.roster,
}),
}),
});The schema above builds three ways from the same definitions. Call toSchema with no subGraph for the full graph, or name one to get its view:
// Everything — the schema you develop against.
const schema = builder.toSchema();
// Public: Team.id and Team.name only. budget, roster, and Player are gone.
const publicSchema = builder.toSchema({ subGraph: 'Public' });
// Internal: the whole graph, since every type is in Internal too.
const internalSchema = builder.toSchema({ subGraph: 'Internal' });The playground embed above builds the Public view, so introspection and the sample query only see id and name on a team. Add budget to the query and it fails to validate; that field exists only in the internal graph.
Combining sub-graphs
toSchema also takes a list of sub-graphs. An array is a union: a type is kept if it belongs to any of the named sub-graphs:
// Every type/field tagged Internal OR Public.
const combined = builder.toSchema({ subGraph: ['Internal', 'Public'] });The { all: [...] } form is the intersection: a type is kept only if it belongs to every named sub-graph:
// Only what is shared by BOTH Internal AND Public.
const shared = builder.toSchema({ subGraph: { all: ['Internal', 'Public'] } });Where membership comes from
A field's sub-graphs are resolved in order, first match wins:
- The field's own
subGraphsarray. - The parent type's
defaultSubGraphsForFields. - The parent type's own
subGraphs, if the builder setfieldsInheritFromTypes: true. - The builder's
subGraphs.defaultForFields. - Otherwise an empty array; the field is in no sub-graph.
Set defaultSubGraphsForFields on a type to give its fields a starting point that differs from the type itself. A Query type can live in every sub-graph while its fields default to none, so each field has to opt in explicitly:
builder.queryType({
// The Query type is reachable from every sub-graph...
subGraphs: ['Public', 'Internal'],
// ...but its fields join nothing unless they say so.
defaultSubGraphsForFields: [],
fields: (t) => ({
teams: t.field({
type: [Team],
// Present in the default and Internal schemas, absent from Public.
subGraphs: ['Internal'],
resolve: () => [...Teams.values()],
}),
}),
});Type and field options
| Where | Option | Purpose |
|---|---|---|
| Any type | subGraphs | The sub-graphs this type belongs to. Falls back to the builder's defaultForTypes. |
| Object / interface / root type | defaultSubGraphsForFields | Default membership for this type's fields, before fieldsInheritFromTypes. |
| Field | subGraphs | The sub-graphs this field belongs to. Falls back through the chain above. |
| Nullable arg / input field | subGraphs | The sub-graphs this argument or input field belongs to. Non-null args and input fields cannot be removed (see below). |
Builder options
| Option | Purpose |
|---|---|
subGraphs.defaultForTypes | Sub-graphs a type joins when it has no subGraphs of its own. |
subGraphs.defaultForFields | Sub-graphs a field joins when nothing earlier in the chain applies. |
subGraphs.fieldsInheritFromTypes | Defaults to false. When true, a field with no membership of its own inherits its parent type's sub-graphs, but only when the type has no defaultSubGraphsForFields. |
subGraphs.explicitlyIncludeType | A predicate to force otherwise-unreachable types into a sub-graph (see Unreachable types). |
Missing types
Building a sub-graph copies in only the types and output fields that belong to it. Everything else is dropped. Input types are the exception: an argument or input field is only removable when it is nullable, because a resolver still expects its non-null arguments to be present. The plugin throws at build time if you try to strip a non-null argument or input field from a sub-graph that keeps its field.
Output fields and structural references fail differently when they point at a dropped type. An output field whose return type was left out is pruned automatically, with no error; that's how roster, returning the Internal-only Player, vanishes from the Public view above. A runtime error is thrown only when a dropped type is reached as an interface implemented by another interface, a union member, or a non-null argument type; those references can't be rewritten around the missing type. Tag the referencing field or type out of that sub-graph too; fields drop on their own, but the structural references hard-fail.
Unreachable types
Filtering keeps a type only when something in the sub-graph reaches it. A type that nothing references is dropped even if it's tagged for the sub-graph. explicitlyIncludeType overrides that: return true for any type you want kept regardless of reachability.
The case this exists for is federation. When you extend an external reference with the federation plugin, the externalRef may not be reachable through your own schema, yet the built sub-graph still needs it. Keep every type that carries a resolvable key directive:
import FederationPlugin, { hasResolvableKey } from '@pothos/plugin-federation';
import SubGraphPlugin from '@pothos/plugin-sub-graph';
const builder = new SchemaBuilder<{
SubGraphs: 'Public' | 'Internal';
}>({
plugins: [SubGraphPlugin, FederationPlugin],
subGraphs: {
explicitlyIncludeType: (type, subGraphs) => hasResolvableKey(type),
},
});