import { GraphQLBoolean, type GraphQLDirective, GraphQLFloat, GraphQLID, GraphQLInt, type GraphQLObjectType, type GraphQLScalarSerializer, type GraphQLScalarType, GraphQLSchema, GraphQLString, type GraphQLTypeResolver, lexicographicSortSchema, } from 'graphql'; import { BuildCache } from './build-cache'; import { ConfigStore } from './config-store'; import { PothosError } from './errors'; import { InputFieldBuilder } from './fieldUtils/input'; import { InterfaceFieldBuilder } from './fieldUtils/interface'; import { MutationFieldBuilder } from './fieldUtils/mutation'; import { ObjectFieldBuilder } from './fieldUtils/object'; import { QueryFieldBuilder } from './fieldUtils/query'; import { SubscriptionFieldBuilder } from './fieldUtils/subscription'; import { BaseTypeRef } from './refs/base'; import { EnumRef } from './refs/enum'; import { ImplementableInputObjectRef, InputObjectRef } from './refs/input-object'; import { ImplementableInterfaceRef, InterfaceRef } from './refs/interface'; import { MutationRef } from './refs/mutation'; import { ImplementableObjectRef, ObjectRef } from './refs/object'; import { QueryRef } from './refs/query'; import { ScalarRef } from './refs/scalar'; import { SubscriptionRef } from './refs/subscription'; import { UnionRef } from './refs/union'; import type { AbstractReturnShape, AddVersionedDefaultsToBuilderOptions, BaseEnum, ConfigurableRef, EnumParam, EnumTypeOptions, EnumValues, InputFieldMap, InputFieldsFromShape, InputShape, InputShapeFromFields, InterfaceFieldsShape, InterfaceFieldThunk, InterfaceParam, InterfaceTypeOptions, MutationFieldsShape, MutationFieldThunk, NormalizeArgs, NormalizeSchemeBuilderOptions, ObjectFieldsShape, ObjectFieldThunk, ObjectParam, ObjectTypeOptions, OneOfInputShapeFromFields, OutputShape, ParentShape, PluginConstructorMap, PothosInputObjectTypeConfig, QueryFieldsShape, QueryFieldThunk, RecursivelyNormalizeNullableFields, ScalarName, SchemaTypes, ShapeFromEnumValues, SubscriptionFieldsShape, SubscriptionFieldThunk, ValuesFromEnum, } from './types'; import { normalizeEnumValues, valuesFromEnum, verifyInterfaces, verifyRef } from './utils'; export class SchemaBuilder { $inferSchemaTypes!: Types; private queryRef = new QueryRef('Query'); private mutationRef = new MutationRef('Mutation'); private subscriptionRef = new SubscriptionRef('Subscription'); static plugins: Partial> = {}; static optionNormalizers: Map< string, { v3?: ( options: AddVersionedDefaultsToBuilderOptions, ) => Partial>; v4?: undefined; } > = new Map(); static allowPluginReRegistration = false; configStore: ConfigStore; options: PothosSchemaTypes.SchemaBuilderOptions; defaultFieldNullability: boolean; defaultInputFieldRequiredness: boolean; constructor(options: PothosSchemaTypes.SchemaBuilderOptions) { this.options = [...SchemaBuilder.optionNormalizers.values()].reduce((opts, normalize) => { if (options.defaults && typeof normalize[options.defaults] === 'function') { return Object.assign(opts, normalize[options.defaults]!(opts)); } return opts; }, options); this.configStore = new ConfigStore(this); this.defaultFieldNullability = ( options as { defaultFieldNullability?: boolean; } ).defaultFieldNullability ?? options.defaults !== 'v3'; this.defaultInputFieldRequiredness = ( options as { defaultInputFieldRequiredness?: boolean; } ).defaultInputFieldRequiredness ?? false; } static registerPlugin>( name: T, plugin: PluginConstructorMap[T], normalizeOptions?: { v3?: ( options: AddVersionedDefaultsToBuilderOptions, ) => Partial>; }, ) { if (!SchemaBuilder.allowPluginReRegistration && SchemaBuilder.plugins[name]) { throw new PothosError(`Received multiple implementations for plugin ${name}`); } SchemaBuilder.plugins[name] = plugin; if (normalizeOptions) { SchemaBuilder.optionNormalizers.set(name, normalizeOptions); } } objectType[], Param extends ObjectParam>( param: Param, options: ObjectTypeOptions, Interfaces>, fields?: ObjectFieldsShape>, ): PothosSchemaTypes.ObjectRef, ParentShape> { verifyRef(param); verifyInterfaces(options.interfaces); const name = typeof param === 'string' ? param : ((options as { name?: string }).name ?? (param as { name: string }).name); const ref = param instanceof BaseTypeRef ? (param as ObjectRef, ParentShape>) : new ObjectRef, ParentShape>(name); ref.updateConfig({ kind: 'Object', graphqlKind: 'Object', name, interfaces: [], description: options.description, extensions: options.extensions, astNode: options.astNode, isTypeOf: options.isTypeOf, pothosOptions: options as PothosSchemaTypes.ObjectTypeOptions, }); if (options.interfaces) { ref.addInterfaces(options.interfaces); } if (ref !== param && typeof param !== 'string') { this.configStore.associateParamWithRef(param, ref); } if (fields) { ref.addFields(() => fields(new ObjectFieldBuilder>(this))); } if (options.fields) { ref.addFields(() => { const t = new ObjectFieldBuilder>(this); return options.fields!(t); }); } this.configStore.addTypeRef(ref); return ref; } objectFields>( param: Type, fields: ObjectFieldsShape>, ) { verifyRef(param); this.configStore.addFields(param, () => fields(new ObjectFieldBuilder>(this)), ); } objectField>( param: Type, fieldName: string, field: ObjectFieldThunk>, ) { verifyRef(param); this.configStore.addFields(param, () => ({ [fieldName]: field(new ObjectFieldBuilder>(this)), })); } queryType( ...args: NormalizeArgs< [options: PothosSchemaTypes.QueryTypeOptions, fields?: QueryFieldsShape], 0 > ): QueryRef { const [options = {}, fields] = args; this.queryRef.updateConfig({ kind: 'Query', graphqlKind: 'Object', name: options.name ?? 'Query', description: options.description, pothosOptions: options as unknown as PothosSchemaTypes.QueryTypeOptions, extensions: options.extensions, astNode: options.astNode, }); if (options.name) { this.queryRef.name = options.name; } this.configStore.addTypeRef(this.queryRef); if (fields) { this.queryRef.addFields(() => fields(new QueryFieldBuilder(this))); } if (options.fields) { this.queryRef.addFields(() => options.fields!(new QueryFieldBuilder(this))); } return this.queryRef; } queryFields(fields: QueryFieldsShape) { this.configStore.addFields(this.queryRef, () => fields(new QueryFieldBuilder(this))); } queryField(name: string, field: QueryFieldThunk) { this.configStore.addFields(this.queryRef, () => ({ [name]: field(new QueryFieldBuilder(this)), })); } mutationType( ...args: NormalizeArgs< [options: PothosSchemaTypes.MutationTypeOptions, fields?: MutationFieldsShape], 0 > ) { const [options = {}, fields] = args; this.mutationRef.updateConfig({ kind: 'Mutation', graphqlKind: 'Object', name: options.name ?? 'Mutation', description: options.description, pothosOptions: options as unknown as PothosSchemaTypes.MutationTypeOptions, extensions: options.extensions, astNode: options.astNode, }); this.configStore.addTypeRef(this.mutationRef); if (options.name) { this.mutationRef.name = options.name; } if (fields) { this.configStore.addFields(this.mutationRef, () => fields(new MutationFieldBuilder(this))); } if (options.fields) { this.configStore.addFields(this.mutationRef, () => options.fields!(new MutationFieldBuilder(this)), ); } return this.mutationRef; } mutationFields(fields: MutationFieldsShape) { this.configStore.addFields(this.mutationRef, () => fields(new MutationFieldBuilder(this))); } mutationField(name: string, field: MutationFieldThunk) { this.configStore.addFields(this.mutationRef, () => ({ [name]: field(new MutationFieldBuilder(this)), })); } subscriptionType( ...args: NormalizeArgs< [ options: PothosSchemaTypes.SubscriptionTypeOptions, fields?: SubscriptionFieldsShape, ], 0 > ) { const [options = {}, fields] = args; this.subscriptionRef.updateConfig({ kind: 'Subscription', graphqlKind: 'Object', name: options.name ?? 'Subscription', description: options.description, pothosOptions: options as unknown as PothosSchemaTypes.SubscriptionTypeOptions, extensions: options.extensions, astNode: options.astNode, }); this.configStore.addTypeRef(this.subscriptionRef); if (options.name) { this.subscriptionRef.name = options.name; } if (fields) { this.configStore.addFields(this.subscriptionRef, () => fields(new SubscriptionFieldBuilder(this)), ); } if (options.fields) { this.configStore.addFields(this.subscriptionRef, () => options.fields!(new SubscriptionFieldBuilder(this)), ); } return this.subscriptionRef; } subscriptionFields(fields: SubscriptionFieldsShape) { this.configStore.addFields(this.subscriptionRef, () => fields(new SubscriptionFieldBuilder(this)), ); } subscriptionField(name: string, field: SubscriptionFieldThunk) { this.configStore.addFields(this.subscriptionRef, () => ({ [name]: field(new SubscriptionFieldBuilder(this)), })); } args( fields: (t: PothosSchemaTypes.InputFieldBuilder) => Shape, ): Shape { return fields(new InputFieldBuilder(this, 'Arg')); } interfaceType< Param extends InterfaceParam, const Interfaces extends InterfaceParam[], ResolveType, >( param: Param, options: InterfaceTypeOptions, Interfaces, ResolveType>, fields?: InterfaceFieldsShape>, ): PothosSchemaTypes.InterfaceRef< Types, AbstractReturnShape, ParentShape > { verifyRef(param); verifyInterfaces(options.interfaces); const name = typeof param === 'string' ? param : ((options as { name?: string }).name ?? (param as { name: string }).name); const ref = param instanceof BaseTypeRef ? (param as InterfaceRef< Types, AbstractReturnShape, ParentShape >) : new InterfaceRef< Types, AbstractReturnShape, ParentShape >(name); const typename = ref.name; ref.updateConfig({ kind: 'Interface', graphqlKind: 'Interface', name: typename, interfaces: [], description: options.description, pothosOptions: options as unknown as PothosSchemaTypes.InterfaceTypeOptions, extensions: options.extensions, resolveType: options.resolveType as GraphQLTypeResolver, astNode: options.astNode, }); this.configStore.addTypeRef(ref); if (options.interfaces) { ref.addInterfaces(options.interfaces); } if (ref !== param && typeof param !== 'string') { this.configStore.associateParamWithRef(param, ref); } if (fields) { this.configStore.addFields(ref, () => fields(new InterfaceFieldBuilder(this))); } if (options.fields) { this.configStore.addFields(ref, () => options.fields!(new InterfaceFieldBuilder(this))); } return ref; } interfaceFields>( ref: Type, fields: InterfaceFieldsShape>, ) { verifyRef(ref); this.configStore.addFields(ref, () => fields(new InterfaceFieldBuilder(this))); } interfaceField>( ref: Type, fieldName: string, field: InterfaceFieldThunk>, ) { verifyRef(ref); this.configStore.addFields(ref, () => ({ [fieldName]: field(new InterfaceFieldBuilder(this)), })); } unionType, ResolveType>( name: string, options: PothosSchemaTypes.UnionTypeOptions, ): PothosSchemaTypes.UnionRef< Types, AbstractReturnShape, ParentShape > { const ref = new UnionRef< Types, AbstractReturnShape, ParentShape >(name, { kind: 'Union', graphqlKind: 'Union', name, types: [], description: options.description, resolveType: options.resolveType as GraphQLTypeResolver, pothosOptions: options as unknown as PothosSchemaTypes.UnionTypeOptions, extensions: options.extensions, astNode: options.astNode, }); if (Array.isArray(options.types)) { for (const type of options.types) { verifyRef(type); } } this.configStore.addTypeRef(ref); ref.addTypes(options.types); return ref; } enumType>( param: Param, options: EnumTypeOptions, ): PothosSchemaTypes.EnumRef< Types, Param extends BaseEnum ? ValuesFromEnum : ShapeFromEnumValues > { verifyRef(param); const name = typeof param === 'string' ? param : (options as { name: string }).name; const values = typeof param === 'object' ? valuesFromEnum( param as BaseEnum, options?.values as Record>, ) : normalizeEnumValues((options as { values: EnumValues }).values); const ref = new EnumRef< Types, Param extends BaseEnum ? ValuesFromEnum : ShapeFromEnumValues >(name, { kind: 'Enum', graphqlKind: 'Enum', name, values, description: options.description, pothosOptions: options as unknown as PothosSchemaTypes.EnumTypeOptions, extensions: options.extensions, astNode: options.astNode, }); this.configStore.addTypeRef(ref); if (typeof param !== 'string') { this.configStore.associateParamWithRef(param as ConfigurableRef, ref); } return ref; } scalarType>( name: Name, options: PothosSchemaTypes.ScalarTypeOptions< Types, InputShape, ParentShape >, ): PothosSchemaTypes.ScalarRef, ParentShape> { const ref = new ScalarRef, ParentShape>(name, { kind: 'Scalar', graphqlKind: 'Scalar', name, description: options.description, parseLiteral: options.parseLiteral, parseValue: options.parseValue, serialize: options.serialize as GraphQLScalarSerializer>, pothosOptions: options as unknown as PothosSchemaTypes.ScalarTypeOptions, extensions: options.extensions, astNode: options.astNode, }); this.configStore.addTypeRef(ref); return ref; } addScalarType>( name: Name, scalar: GraphQLScalarType, ...args: NormalizeArgs< [ options: Omit< PothosSchemaTypes.ScalarTypeOptions< Types, InputShape, OutputShape >, 'serialize' > & { serialize?: GraphQLScalarSerializer>; }, ] > ) { const [options = {}] = args; const config = scalar.toConfig(); return this.scalarType(name, { ...config, ...options, extensions: { ...config.extensions, ...options.extensions, }, } as PothosSchemaTypes.ScalarTypeOptions< Types, InputShape, ParentShape >); } inputType< Param extends InputObjectRef | string, Fields extends Param extends PothosSchemaTypes.InputObjectRef ? InputFieldsFromShape & object, 'InputObject'> : Param extends keyof Types['Inputs'] ? InputFieldsFromShape & object, 'InputObject'> : InputFieldMap, IsOneOf extends boolean = boolean, >( param: Param, options: PothosSchemaTypes.InputObjectTypeOptions & { isOneOf?: IsOneOf; }, ): PothosSchemaTypes.InputObjectRef< Types, [IsOneOf] extends [true] ? OneOfInputShapeFromFields : InputShapeFromFields > { verifyRef(param); const name = typeof param === 'string' ? param : (param as { name: string }).name; const ref = ( typeof param === 'string' ? new InputObjectRef>(name) : param ) as PothosSchemaTypes.InputObjectRef< Types, [IsOneOf] extends [true] ? OneOfInputShapeFromFields : InputShapeFromFields >; ref.updateConfig({ kind: 'InputObject', graphqlKind: 'InputObject', name, isOneOf: options.isOneOf, description: options.description, pothosOptions: options as unknown as PothosSchemaTypes.InputObjectTypeOptions, extensions: options.extensions, astNode: options.astNode, } as PothosInputObjectTypeConfig & { isOneOf?: boolean }); this.configStore.addTypeRef(ref); if (param !== ref && typeof param !== 'string') { this.configStore.associateParamWithRef(param as ConfigurableRef, ref); } this.configStore.addInputFields(ref, () => options.fields(new InputFieldBuilder(this, 'InputObject')), ); return ref; } inputRef( name: string, ): PothosSchemaTypes.ImplementableInputObjectRef< Types, RecursivelyNormalizeNullableFields, Normalize extends false ? T : RecursivelyNormalizeNullableFields > { return new ImplementableInputObjectRef< Types, RecursivelyNormalizeNullableFields, Normalize extends false ? T : RecursivelyNormalizeNullableFields >(this, name); } objectRef(name: string): PothosSchemaTypes.ImplementableObjectRef { return new ImplementableObjectRef(this, name); } interfaceRef(name: string): PothosSchemaTypes.ImplementableInterfaceRef { return new ImplementableInterfaceRef(this, name); } toSchema(...args: NormalizeArgs<[options?: PothosSchemaTypes.BuildSchemaOptions]>) { const [options = {}] = args; const { directives, extensions } = options; const scalars = [GraphQLID, GraphQLInt, GraphQLFloat, GraphQLString, GraphQLBoolean]; for (const scalar of scalars) { if (!this.configStore.hasImplementation(scalar.name)) { this.addScalarType(scalar.name as ScalarName, scalar); } } const buildCache = new BuildCache(this, options); buildCache.plugin.beforeBuild(); buildCache.buildAll(); const builtTypes = [...buildCache.types.values()]; const queryName = this.configStore.hasConfig(this.queryRef) ? this.configStore.getTypeConfig(this.queryRef).name : 'Query'; const mutationName = this.configStore.hasConfig(this.mutationRef) ? this.configStore.getTypeConfig(this.mutationRef).name : 'Mutation'; const subscriptionName = this.configStore.hasConfig(this.subscriptionRef) ? this.configStore.getTypeConfig(this.subscriptionRef).name : 'Subscription'; const schema = new GraphQLSchema({ query: buildCache.types.get(queryName) as GraphQLObjectType | undefined, mutation: buildCache.types.get(mutationName) as GraphQLObjectType | undefined, subscription: buildCache.types.get(subscriptionName) as GraphQLObjectType | undefined, extensions: extensions ?? {}, directives: directives as GraphQLDirective[], types: builtTypes, astNode: options.astNode, }); const processedSchema = buildCache.plugin.afterBuild(schema); return options.sortSchema === false ? processedSchema : lexicographicSortSchema(processedSchema); } }