UNPKG

@pothos/core

Version:

Pothos (formerly GiraphQL) is a plugin based schema builder for creating code-first GraphQL schemas in typescript

719 lines (609 loc) 21.6 kB
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<Types extends SchemaTypes> { $inferSchemaTypes!: Types; private queryRef = new QueryRef<Types>('Query'); private mutationRef = new MutationRef<Types>('Mutation'); private subscriptionRef = new SubscriptionRef<Types>('Subscription'); static plugins: Partial<PluginConstructorMap<SchemaTypes>> = {}; static optionNormalizers: Map< string, { v3?: ( options: AddVersionedDefaultsToBuilderOptions<SchemaTypes, 'v3'>, ) => Partial<NormalizeSchemeBuilderOptions<SchemaTypes>>; v4?: undefined; } > = new Map(); static allowPluginReRegistration = false; configStore: ConfigStore<Types>; options: PothosSchemaTypes.SchemaBuilderOptions<Types>; defaultFieldNullability: boolean; defaultInputFieldRequiredness: boolean; constructor(options: PothosSchemaTypes.SchemaBuilderOptions<Types>) { this.options = [...SchemaBuilder.optionNormalizers.values()].reduce((opts, normalize) => { if (options.defaults && typeof normalize[options.defaults] === 'function') { // biome-ignore lint/performance/noAccumulatingSpread: this is fine return Object.assign(opts, normalize[options.defaults]!(opts)); } return opts; }, options); this.configStore = new ConfigStore<Types>(this); this.defaultFieldNullability = ( options as { defaultFieldNullability?: boolean; } ).defaultFieldNullability ?? options.defaults !== 'v3'; this.defaultInputFieldRequiredness = ( options as { defaultInputFieldRequiredness?: boolean; } ).defaultInputFieldRequiredness ?? false; } static registerPlugin<T extends keyof PluginConstructorMap<SchemaTypes>>( name: T, plugin: PluginConstructorMap<SchemaTypes>[T], normalizeOptions?: { v3?: ( options: AddVersionedDefaultsToBuilderOptions<SchemaTypes, 'v3'>, ) => Partial<NormalizeSchemeBuilderOptions<SchemaTypes>>; }, ) { 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<const Interfaces extends InterfaceParam<Types>[], Param extends ObjectParam<Types>>( param: Param, options: ObjectTypeOptions<Types, Param, ParentShape<Types, Param>, Interfaces>, fields?: ObjectFieldsShape<Types, ParentShape<Types, Param>>, ): PothosSchemaTypes.ObjectRef<Types, OutputShape<Types, Param>, ParentShape<Types, Param>> { 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<Types, OutputShape<Types, Param>, ParentShape<Types, Param>>) : new ObjectRef<Types, OutputShape<Types, Param>, ParentShape<Types, Param>>(name); ref.updateConfig({ kind: 'Object', graphqlKind: 'Object', name, interfaces: [], description: options.description, extensions: options.extensions, 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<Types, ParentShape<Types, Param>>(this))); } if (options.fields) { ref.addFields(() => { const t = new ObjectFieldBuilder<Types, ParentShape<Types, Param>>(this); return options.fields!(t); }); } this.configStore.addTypeRef(ref); return ref; } objectFields<Type extends ObjectParam<Types>>( param: Type, fields: ObjectFieldsShape<Types, ParentShape<Types, Type>>, ) { verifyRef(param); this.configStore.addFields(param, () => fields(new ObjectFieldBuilder<Types, ParentShape<Types, Type>>(this)), ); } objectField<Type extends ObjectParam<Types>>( param: Type, fieldName: string, field: ObjectFieldThunk<Types, ParentShape<Types, Type>>, ) { verifyRef(param); this.configStore.addFields(param, () => ({ [fieldName]: field(new ObjectFieldBuilder<Types, ParentShape<Types, Type>>(this)), })); } queryType( ...args: NormalizeArgs< [options: PothosSchemaTypes.QueryTypeOptions<Types>, fields?: QueryFieldsShape<Types>], 0 > ): QueryRef<Types> { 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, }); 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<Types>) { this.configStore.addFields(this.queryRef, () => fields(new QueryFieldBuilder(this))); } queryField(name: string, field: QueryFieldThunk<Types>) { this.configStore.addFields(this.queryRef, () => ({ [name]: field(new QueryFieldBuilder(this)), })); } mutationType( ...args: NormalizeArgs< [options: PothosSchemaTypes.MutationTypeOptions<Types>, fields?: MutationFieldsShape<Types>], 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, }); 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<Types>) { this.configStore.addFields(this.mutationRef, () => fields(new MutationFieldBuilder(this))); } mutationField(name: string, field: MutationFieldThunk<Types>) { this.configStore.addFields(this.mutationRef, () => ({ [name]: field(new MutationFieldBuilder(this)), })); } subscriptionType( ...args: NormalizeArgs< [ options: PothosSchemaTypes.SubscriptionTypeOptions<Types>, fields?: SubscriptionFieldsShape<Types>, ], 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, }); 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<Types>) { this.configStore.addFields(this.subscriptionRef, () => fields(new SubscriptionFieldBuilder(this)), ); } subscriptionField(name: string, field: SubscriptionFieldThunk<Types>) { this.configStore.addFields(this.subscriptionRef, () => ({ [name]: field(new SubscriptionFieldBuilder(this)), })); } args<Shape extends InputFieldMap>( fields: (t: PothosSchemaTypes.InputFieldBuilder<Types, 'Arg'>) => Shape, ): Shape { return fields(new InputFieldBuilder<Types, 'Arg'>(this, 'Arg')); } interfaceType< Param extends InterfaceParam<Types>, const Interfaces extends InterfaceParam<Types>[], ResolveType, >( param: Param, options: InterfaceTypeOptions<Types, Param, ParentShape<Types, Param>, Interfaces, ResolveType>, fields?: InterfaceFieldsShape<Types, ParentShape<Types, Param>>, ): PothosSchemaTypes.InterfaceRef< Types, AbstractReturnShape<Types, Param, ResolveType>, ParentShape<Types, Param> > { 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<Types, Param, ResolveType>, ParentShape<Types, Param> >) : new InterfaceRef< Types, AbstractReturnShape<Types, Param, ResolveType>, ParentShape<Types, Param> >(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<unknown, unknown>, }); 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<Type extends InterfaceParam<Types>>( ref: Type, fields: InterfaceFieldsShape<Types, ParentShape<Types, Type>>, ) { verifyRef(ref); this.configStore.addFields(ref, () => fields(new InterfaceFieldBuilder(this))); } interfaceField<Type extends InterfaceParam<Types>>( ref: Type, fieldName: string, field: InterfaceFieldThunk<Types, ParentShape<Types, Type>>, ) { verifyRef(ref); this.configStore.addFields(ref, () => ({ [fieldName]: field(new InterfaceFieldBuilder(this)), })); } unionType<Member extends ObjectParam<Types>, ResolveType>( name: string, options: PothosSchemaTypes.UnionTypeOptions<Types, Member, ResolveType>, ): PothosSchemaTypes.UnionRef< Types, AbstractReturnShape<Types, Member, ResolveType>, ParentShape<Types, Member> > { const ref = new UnionRef< Types, AbstractReturnShape<Types, Member, ResolveType>, ParentShape<Types, Member> >(name, { kind: 'Union', graphqlKind: 'Union', name, types: [], description: options.description, resolveType: options.resolveType as GraphQLTypeResolver<unknown, object>, pothosOptions: options as unknown as PothosSchemaTypes.UnionTypeOptions, extensions: options.extensions, }); 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 extends EnumParam, const Values extends EnumValues<Types>>( param: Param, options: EnumTypeOptions<Types, Param, Values>, ): PothosSchemaTypes.EnumRef< Types, Param extends BaseEnum ? ValuesFromEnum<Param> : ShapeFromEnumValues<Types, Values> > { verifyRef(param); const name = typeof param === 'string' ? param : (options as { name: string }).name; const values = typeof param === 'object' ? valuesFromEnum<Types>( param as BaseEnum, options?.values as Record<string, PothosSchemaTypes.EnumValueConfig<Types>>, ) : normalizeEnumValues<Types>((options as { values: EnumValues<Types> }).values); const ref = new EnumRef< Types, Param extends BaseEnum ? ValuesFromEnum<Param> : ShapeFromEnumValues<Types, Values> >(name, { kind: 'Enum', graphqlKind: 'Enum', name, values, description: options.description, pothosOptions: options as unknown as PothosSchemaTypes.EnumTypeOptions<Types>, extensions: options.extensions, }); this.configStore.addTypeRef(ref); if (typeof param !== 'string') { this.configStore.associateParamWithRef(param as ConfigurableRef<Types>, ref); } return ref; } scalarType<Name extends ScalarName<Types>>( name: Name, options: PothosSchemaTypes.ScalarTypeOptions< Types, InputShape<Types, Name>, ParentShape<Types, Name> >, ): PothosSchemaTypes.ScalarRef<Types, InputShape<Types, Name>, ParentShape<Types, Name>> { const ref = new ScalarRef<Types, InputShape<Types, Name>, ParentShape<Types, Name>>(name, { kind: 'Scalar', graphqlKind: 'Scalar', name, description: options.description, parseLiteral: options.parseLiteral, parseValue: options.parseValue, serialize: options.serialize as GraphQLScalarSerializer<OutputShape<Types, Name>>, pothosOptions: options as unknown as PothosSchemaTypes.ScalarTypeOptions, extensions: options.extensions, }); this.configStore.addTypeRef(ref); return ref; } addScalarType<Name extends ScalarName<Types>>( name: Name, scalar: GraphQLScalarType, ...args: NormalizeArgs< [ options: Omit< PothosSchemaTypes.ScalarTypeOptions< Types, InputShape<Types, Name>, OutputShape<Types, Name> >, 'serialize' > & { serialize?: GraphQLScalarSerializer<OutputShape<Types, Name>>; }, ] > ) { const [options = {}] = args; const config = scalar.toConfig(); return this.scalarType<Name>(name, { ...config, ...options, extensions: { ...config.extensions, ...options.extensions, }, } as PothosSchemaTypes.ScalarTypeOptions< Types, InputShape<Types, Name>, ParentShape<Types, Name> >); } inputType< Param extends InputObjectRef<Types, unknown> | string, Fields extends Param extends PothosSchemaTypes.InputObjectRef<Types, unknown> ? InputFieldsFromShape<Types, InputShape<Types, Param> & object, 'InputObject'> : Param extends keyof Types['Inputs'] ? InputFieldsFromShape<Types, InputShape<Types, Param> & object, 'InputObject'> : InputFieldMap, IsOneOf extends boolean = boolean, >( param: Param, options: PothosSchemaTypes.InputObjectTypeOptions<Types, Fields> & { isOneOf?: IsOneOf; }, ): PothosSchemaTypes.InputObjectRef< Types, [IsOneOf] extends [true] ? OneOfInputShapeFromFields<Fields> : InputShapeFromFields<Fields> > { verifyRef(param); const name = typeof param === 'string' ? param : (param as { name: string }).name; const ref = ( typeof param === 'string' ? new InputObjectRef<Types, InputShapeFromFields<Fields>>(name) : param ) as PothosSchemaTypes.InputObjectRef< Types, [IsOneOf] extends [true] ? OneOfInputShapeFromFields<Fields> : InputShapeFromFields<Fields> >; ref.updateConfig({ kind: 'InputObject', graphqlKind: 'InputObject', name, isOneOf: options.isOneOf, description: options.description, pothosOptions: options as unknown as PothosSchemaTypes.InputObjectTypeOptions, extensions: options.extensions, } as PothosInputObjectTypeConfig & { isOneOf?: boolean }); this.configStore.addTypeRef(ref); if (param !== ref && typeof param !== 'string') { this.configStore.associateParamWithRef(param as ConfigurableRef<Types>, ref); } this.configStore.addInputFields(ref, () => options.fields(new InputFieldBuilder(this, 'InputObject')), ); return ref; } inputRef<T extends object, Normalize = true>( name: string, ): ImplementableInputObjectRef< Types, RecursivelyNormalizeNullableFields<T>, Normalize extends false ? T : RecursivelyNormalizeNullableFields<T> > { return new ImplementableInputObjectRef< Types, RecursivelyNormalizeNullableFields<T>, Normalize extends false ? T : RecursivelyNormalizeNullableFields<T> >(this, name); } objectRef<T>(name: string): ImplementableObjectRef<Types, T> { return new ImplementableObjectRef<Types, T>(this, name); } interfaceRef<T>(name: string): ImplementableInterfaceRef<Types, T> { return new ImplementableInterfaceRef<Types, T>(this, name); } toSchema(...args: NormalizeArgs<[options?: PothosSchemaTypes.BuildSchemaOptions<Types>]>) { 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<Types>, 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, }); const processedSchema = buildCache.plugin.afterBuild(schema); return options.sortSchema === false ? processedSchema : lexicographicSortSchema(processedSchema); } }