@pothos/plugin-sub-graph
Version:
A Pothos plugin for creating multiple variants or sub-selections of the same graph
374 lines (326 loc) • 11.4 kB
text/typescript
import './global-types';
import SchemaBuilder, {
BasePlugin,
type PothosInputFieldConfig,
type PothosOutputFieldConfig,
PothosSchemaError,
type PothosTypeConfig,
type SchemaTypes,
} from '@pothos/core';
import {
GraphQLEnumType,
type GraphQLFieldConfigArgumentMap,
type GraphQLFieldConfigMap,
type GraphQLInputFieldConfigMap,
GraphQLInputObjectType,
GraphQLInterfaceType,
type GraphQLNamedType,
GraphQLObjectType,
GraphQLScalarType,
GraphQLSchema,
GraphQLUnionType,
getNamedType,
isInterfaceType,
isNonNullType,
isObjectType,
} from 'graphql';
import { replaceType } from './util';
const pluginName = 'subGraph';
export default pluginName;
function matchesSubGraphs(left: string[], right: string[], mode: 'any' | 'all') {
if (mode === 'all') {
return right.every((entry) => left.includes(entry));
}
for (const entry of left) {
if (right.includes(entry)) {
return true;
}
}
return false;
}
export class PothosSubGraphPlugin<Types extends SchemaTypes> extends BasePlugin<Types> {
static createSubGraph<Types extends SchemaTypes>(
schema: GraphQLSchema,
subGraph: string[] | string | { all: string[] },
builder: PothosSchemaTypes.SchemaBuilder<Types>,
) {
const mode: 'any' | 'all' = Array.isArray(subGraph)
? 'any'
: typeof subGraph === 'string'
? 'any'
: 'all';
const subGraphs = Array.isArray(subGraph)
? subGraph
: typeof subGraph === 'string'
? [subGraph]
: subGraph.all;
const config = schema.toConfig();
const newTypes = PothosSubGraphPlugin.filterTypes(config.types, subGraphs, mode);
const returnedInterfaces = new Set<string>();
for (const type of newTypes.values()) {
if (isObjectType(type) || isInterfaceType(type)) {
const fields = type.getFields();
for (const field of Object.values(fields)) {
const namedType = getNamedType(field.type);
if (isInterfaceType(namedType)) {
returnedInterfaces.add(namedType.name);
}
}
}
}
function hasReturnedInterface(type: GraphQLInterfaceType | GraphQLObjectType): boolean {
for (const iface of type.getInterfaces()) {
if (returnedInterfaces.has(iface.name)) {
return true;
}
if (hasReturnedInterface(iface)) {
return true;
}
}
return false;
}
return new GraphQLSchema({
directives: config.directives,
extensions: config.extensions,
extensionASTNodes: config.extensionASTNodes,
assumeValid: false,
query: newTypes.get(schema.getQueryType()?.name ?? 'Query') as GraphQLObjectType,
mutation: newTypes.get(schema.getMutationType()?.name ?? 'Mutation') as GraphQLObjectType,
subscription: newTypes.get(
schema.getSubscriptionType()?.name ?? 'Subscription',
) as GraphQLObjectType,
// Explicitly include types that implement an interface that can be resolved in the subGraph
types: [...newTypes.values()].filter(
(type) =>
builder.options.subGraphs?.explicitlyIncludeType?.(type, subGraphs) ||
((isObjectType(type) || isInterfaceType(type)) &&
hasReturnedInterface(type as GraphQLInterfaceType | GraphQLObjectType)),
),
});
}
static filterTypes(types: readonly GraphQLNamedType[], subGraphs: string[], mode: 'any' | 'all') {
const newTypes = new Map<string, GraphQLNamedType>();
for (const type of types) {
if (type.name.startsWith('__')) {
continue;
}
if (
type.name === 'String' ||
type.name === 'Int' ||
type.name === 'Float' ||
type.name === 'Boolean' ||
type.name === 'ID'
) {
newTypes.set(type.name, type);
}
if (!matchesSubGraphs((type.extensions?.subGraphs as string[]) || [], subGraphs, mode)) {
continue;
}
if (type instanceof GraphQLScalarType || type instanceof GraphQLEnumType) {
newTypes.set(type.name, type);
} else if (type instanceof GraphQLObjectType) {
const typeConfig = type.toConfig();
newTypes.set(
type.name,
new GraphQLObjectType({
...typeConfig,
interfaces: () =>
typeConfig.interfaces
.filter((iface) => newTypes.has(iface.name))
.map((iface) => replaceType(iface, newTypes, typeConfig.name, subGraphs)),
fields: PothosSubGraphPlugin.filterFields(type, newTypes, subGraphs, mode),
}),
);
} else if (type instanceof GraphQLInterfaceType) {
const typeConfig = type.toConfig();
newTypes.set(
type.name,
new GraphQLInterfaceType({
...typeConfig,
interfaces: () =>
typeConfig.interfaces.map((iface) =>
replaceType(iface, newTypes, typeConfig.name, subGraphs),
),
fields: PothosSubGraphPlugin.filterFields(type, newTypes, subGraphs, mode),
}),
);
} else if (type instanceof GraphQLUnionType) {
const typeConfig = type.toConfig();
newTypes.set(
type.name,
new GraphQLUnionType({
...typeConfig,
types: () =>
typeConfig.types.map((member) =>
replaceType(member, newTypes, typeConfig.name, subGraphs),
),
}),
);
} else if (type instanceof GraphQLInputObjectType) {
const typeConfig = type.toConfig();
newTypes.set(
type.name,
new GraphQLInputObjectType({
...typeConfig,
fields: PothosSubGraphPlugin.mapInputFields(type, newTypes, subGraphs, mode),
}),
);
}
}
return newTypes;
}
static filterFields(
type: GraphQLInterfaceType | GraphQLObjectType,
newTypes: Map<string, GraphQLNamedType>,
subGraphs: string[],
mode: 'any' | 'all',
) {
const oldFields = type.getFields();
return () => {
const newFields: GraphQLFieldConfigMap<unknown, unknown> = {};
for (const [fieldName, fieldConfig] of Object.entries(oldFields)) {
const newArguments: GraphQLFieldConfigArgumentMap = {};
if (
!matchesSubGraphs(
(fieldConfig.extensions?.subGraphs as string[] | undefined) ?? [],
subGraphs,
mode,
) ||
!newTypes.has(getNamedType(fieldConfig.type).name)
) {
continue;
}
for (const argConfig of fieldConfig.args) {
const argSubGraphs = argConfig.extensions?.subGraphs as string[] | undefined;
if (argSubGraphs && !matchesSubGraphs(argSubGraphs, subGraphs, mode)) {
if (isNonNullType(argConfig.type)) {
throw new PothosSchemaError(
`argument ${argConfig.name} of ${type.name}.${fieldName} is NonNull and must be in included in all sub-graphs that include ${type.name}.${fieldName}`,
);
}
continue;
}
newArguments[argConfig.name] = {
description: argConfig.description,
defaultValue: argConfig.defaultValue,
extensions: argConfig.extensions,
astNode: argConfig.astNode,
deprecationReason: argConfig.deprecationReason,
type: replaceType(
argConfig.type,
newTypes,
`${argConfig.name} argument of ${type.name}.${fieldConfig.name}`,
subGraphs,
),
};
}
newFields[fieldName] = {
description: fieldConfig.description,
resolve: fieldConfig.resolve,
subscribe: fieldConfig.subscribe,
deprecationReason: fieldConfig.deprecationReason,
extensions: fieldConfig.extensions,
astNode: fieldConfig.astNode,
type: replaceType(
fieldConfig.type,
newTypes,
`${type.name}.${fieldConfig.name}`,
subGraphs,
),
args: newArguments,
};
}
return newFields;
};
}
static mapInputFields(
type: GraphQLInputObjectType,
newTypes: Map<string, GraphQLNamedType>,
subGraphs: string[],
mode: 'any' | 'all',
) {
const oldFields = type.getFields();
return () => {
const newFields: GraphQLInputFieldConfigMap = {};
for (const [fieldName, fieldConfig] of Object.entries(oldFields)) {
const fieldSubGraphs = fieldConfig.extensions?.subGraphs as string[] | undefined;
if (fieldSubGraphs && !matchesSubGraphs(fieldSubGraphs, subGraphs, mode)) {
if (isNonNullType(fieldConfig.type)) {
throw new PothosSchemaError(
`${type.name}.${fieldName} is NonNull and must be in included in all sub-graphs that include ${type.name}`,
);
}
continue;
}
newFields[fieldName] = {
description: fieldConfig.description,
extensions: fieldConfig.extensions,
astNode: fieldConfig.astNode,
defaultValue: fieldConfig.defaultValue,
deprecationReason: fieldConfig.deprecationReason,
type: replaceType(
fieldConfig.type,
newTypes,
`${type.name}.${fieldConfig.name}`,
subGraphs,
),
};
}
return newFields;
};
}
override afterBuild(schema: GraphQLSchema) {
if (this.options.subGraph) {
return PothosSubGraphPlugin.createSubGraph(schema, this.options.subGraph, this.builder);
}
return schema;
}
override onTypeConfig(typeConfig: PothosTypeConfig) {
return {
...typeConfig,
extensions: {
...typeConfig.extensions,
subGraphs:
typeConfig.pothosOptions.subGraphs ??
this.builder.options.subGraphs?.defaultForTypes ??
[],
},
};
}
override onInputFieldConfig(fieldConfig: PothosInputFieldConfig<Types>) {
if (fieldConfig.pothosOptions.subGraphs) {
return {
...fieldConfig,
extensions: {
...fieldConfig.extensions,
subGraphs: fieldConfig.pothosOptions.subGraphs,
},
};
}
return fieldConfig;
}
override onOutputFieldConfig(fieldConfig: PothosOutputFieldConfig<Types>) {
const typeConfig = this.buildCache.getTypeConfig(fieldConfig.parentType);
if (typeConfig.graphqlKind !== 'Interface' && typeConfig.graphqlKind !== 'Object') {
return fieldConfig;
}
let subGraphs: Types['SubGraphs'][] = [];
if (fieldConfig.pothosOptions.subGraphs) {
subGraphs = fieldConfig.pothosOptions.subGraphs;
} else if (typeConfig.pothosOptions.defaultSubGraphsForFields) {
subGraphs = typeConfig.pothosOptions.defaultSubGraphsForFields;
} else if (this.builder.options.subGraphs?.fieldsInheritFromTypes) {
subGraphs = (typeConfig.extensions?.subGraphs as Types['SubGraphs'][]) || [];
} else if (this.builder.options.subGraphs?.defaultForFields) {
subGraphs = this.builder.options.subGraphs?.defaultForFields;
}
return {
...fieldConfig,
extensions: {
...fieldConfig.extensions,
subGraphs,
},
};
}
}
SchemaBuilder.registerPlugin(pluginName, PothosSubGraphPlugin);