UNPKG

@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
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);