import { GraphQLBoolean, GraphQLDirective, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLIsTypeOfFn, GraphQLObjectType, GraphQLScalarSerializer, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLTypeResolver, lexicographicSortSchema, } from 'graphql'; import BuildCache from './build-cache'; import ConfigStore from './config-store'; import { EnumValues, InputShape, InterfaceFieldsShape, InterfaceFieldThunk, InterfaceParam, MutationFieldsShape, MutationFieldThunk, NormalizeSchemeBuilderOptions, ObjectFieldsShape, ObjectFieldThunk, ObjectParam, OutputShape, OutputType, QueryFieldsShape, QueryFieldThunk, ScalarName, SchemaTypes, ShapeFromEnumValues, SubscriptionFieldsShape, SubscriptionFieldThunk, } from './types'; import { normalizeEnumValues, valuesFromEnum, verifyRef } from './utils'; import { AbstractReturnShape, BaseEnum, EnumParam, EnumRef, EnumTypeOptions, GiraphQLEnumTypeConfig, GiraphQLInputObjectTypeConfig, GiraphQLInterfaceTypeConfig, GiraphQLMutationTypeConfig, GiraphQLObjectTypeConfig, GiraphQLQueryTypeConfig, GiraphQLScalarTypeConfig, GiraphQLSubscriptionTypeConfig, GiraphQLUnionTypeConfig, ImplementableInputObjectRef, ImplementableInterfaceRef, ImplementableObjectRef, InputFieldBuilder, InputFieldMap, InputFieldsFromShape, InputObjectRef, InputShapeFromFields, InterfaceFieldBuilder, InterfaceRef, InterfaceTypeOptions, MutationFieldBuilder, ObjectFieldBuilder, ObjectRef, ObjectTypeOptions, ParentShape, PluginConstructorMap, QueryFieldBuilder, ScalarRef, SubscriptionFieldBuilder, UnionRef, ValuesFromEnum, } from '.'; export default class SchemaBuilder { static plugins: Partial> = {}; static allowPluginReRegistration = false; configStore: ConfigStore; options: NormalizeSchemeBuilderOptions; defaultFieldNullability: boolean; defaultInputFieldRequiredness: boolean; constructor(options: NormalizeSchemeBuilderOptions) { this.options = options; this.configStore = new ConfigStore(); this.defaultFieldNullability = ( options as { defaultFieldNullability?: boolean; } ).defaultFieldNullability ?? false; this.defaultInputFieldRequiredness = ( options as { defaultInputFieldRequiredness?: boolean; } ).defaultInputFieldRequiredness ?? false; } static registerPlugin>( name: T, plugin: PluginConstructorMap[T], ) { if (!this.allowPluginReRegistration && this.plugins[name]) { throw new Error(`Received multiple implementations for plugin ${name}`); } this.plugins[name] = plugin; } objectType[], Param extends ObjectParam>( param: Param, options: ObjectTypeOptions, Interfaces>, fields?: ObjectFieldsShape>, ) { verifyRef(param); const name = typeof param === 'string' ? param : (options as { name?: string }).name ?? (param as { name: string }).name; if (name === 'Query' || name === 'Mutation' || name === 'Subscription') { throw new Error(`Invalid object name ${name} use .create${name}Type() instead`); } const ref = param instanceof ObjectRef ? (param as ObjectRef, ParentShape>) : new ObjectRef, ParentShape>(name); const config: GiraphQLObjectTypeConfig = { kind: 'Object', graphqlKind: 'Object', name, interfaces: (options.interfaces ?? []) as ObjectParam[], description: options.description, extensions: options.extensions, isTypeOf: options.isTypeOf as GraphQLIsTypeOfFn, giraphqlOptions: options as GiraphQLSchemaTypes.ObjectTypeOptions, }; this.configStore.addTypeConfig(config, ref); if (typeof param === 'function') { this.configStore.associateRefWithName(param, name); } if (fields) { this.configStore.addFields(ref, () => fields(new ObjectFieldBuilder>(name, this)), ); } if (options.fields) { this.configStore.addFields(ref, () => { const t = new ObjectFieldBuilder>(name, this); return options.fields!(t); }); } return ref; } objectFields>( ref: Type, fields: ObjectFieldsShape>, ) { verifyRef(ref); this.configStore.onTypeConfig(ref, ({ name }) => { this.configStore.addFields(ref, () => fields(new ObjectFieldBuilder(name, this))); }); } objectField>( ref: Type, fieldName: string, field: ObjectFieldThunk>, ) { verifyRef(ref); this.configStore.onTypeConfig(ref, ({ name }) => { this.configStore.addFields(ref, () => ({ [fieldName]: field(new ObjectFieldBuilder(name, this)), })); }); } queryType( options: GiraphQLSchemaTypes.QueryTypeOptions, fields?: QueryFieldsShape, ) { const config: GiraphQLQueryTypeConfig = { kind: 'Query', graphqlKind: 'Object', name: 'Query', description: options.description, giraphqlOptions: options as unknown as GiraphQLSchemaTypes.QueryTypeOptions, extensions: options.extensions, }; this.configStore.addTypeConfig(config); if (fields) { this.configStore.addFields('Query', () => fields(new QueryFieldBuilder(this))); } if (options.fields) { this.configStore.addFields('Query', () => options.fields!(new QueryFieldBuilder(this))); } } queryFields(fields: QueryFieldsShape) { this.configStore.addFields('Query', () => fields(new QueryFieldBuilder(this))); } queryField(name: string, field: QueryFieldThunk) { this.configStore.addFields('Query', () => ({ [name]: field(new QueryFieldBuilder(this)), })); } mutationType( options: GiraphQLSchemaTypes.MutationTypeOptions, fields?: MutationFieldsShape, ) { const config: GiraphQLMutationTypeConfig = { kind: 'Mutation', graphqlKind: 'Object', name: 'Mutation', description: options.description, giraphqlOptions: options as unknown as GiraphQLSchemaTypes.MutationTypeOptions, extensions: options.extensions, }; this.configStore.addTypeConfig(config); if (fields) { this.configStore.addFields('Mutation', () => fields(new MutationFieldBuilder(this))); } if (options.fields) { this.configStore.addFields('Mutation', () => options.fields!(new MutationFieldBuilder(this))); } } mutationFields(fields: MutationFieldsShape) { this.configStore.addFields('Mutation', () => fields(new MutationFieldBuilder(this))); } mutationField(name: string, field: MutationFieldThunk) { this.configStore.addFields('Mutation', () => ({ [name]: field(new MutationFieldBuilder(this)), })); } subscriptionType( options: GiraphQLSchemaTypes.SubscriptionTypeOptions, fields?: SubscriptionFieldsShape, ) { const config: GiraphQLSubscriptionTypeConfig = { kind: 'Subscription', graphqlKind: 'Object', name: 'Subscription', description: options.description, giraphqlOptions: options as unknown as GiraphQLSchemaTypes.SubscriptionTypeOptions, extensions: options.extensions, }; this.configStore.addTypeConfig(config); if (fields) { this.configStore.addFields('Subscription', () => fields(new SubscriptionFieldBuilder(this))); } if (options.fields) { this.configStore.addFields('Subscription', () => options.fields!(new SubscriptionFieldBuilder(this)), ); } } subscriptionFields(fields: SubscriptionFieldsShape) { this.configStore.addFields('Subscription', () => fields(new SubscriptionFieldBuilder(this))); } subscriptionField(name: string, field: SubscriptionFieldThunk) { this.configStore.addFields('Subscription', () => ({ [name]: field(new SubscriptionFieldBuilder(this)), })); } args( fields: (t: GiraphQLSchemaTypes.InputFieldBuilder) => Shape, ): Shape { return fields(new InputFieldBuilder(this, 'Arg', '[unknown]')); } interfaceType, Interfaces extends InterfaceParam[]>( param: Param, options: InterfaceTypeOptions, Interfaces>, fields?: InterfaceFieldsShape>, ) { verifyRef(param); const name = typeof param === 'string' ? param : (options as { name?: string }).name ?? (param as { name: string }).name; const ref = param instanceof InterfaceRef ? (param as InterfaceRef, ParentShape>) : new InterfaceRef, ParentShape>(name); const typename = ref.name; const config: GiraphQLInterfaceTypeConfig = { kind: 'Interface', graphqlKind: 'Interface', name: typename, interfaces: (options.interfaces ?? []) as ObjectParam[], description: options.description, giraphqlOptions: options as unknown as GiraphQLSchemaTypes.InterfaceTypeOptions, extensions: options.extensions, }; this.configStore.addTypeConfig(config, ref); if (typeof param === 'function') { this.configStore.associateRefWithName(param, name); } if (fields) { this.configStore.addFields(ref, () => fields(new InterfaceFieldBuilder(typename, this))); } if (options.fields) { this.configStore.addFields(ref, () => options.fields!(new InterfaceFieldBuilder(typename, this)), ); } return ref; } interfaceFields>( ref: Type, fields: InterfaceFieldsShape>, ) { verifyRef(ref); this.configStore.onTypeConfig(ref, ({ name }) => { this.configStore.addFields(ref, () => fields(new InterfaceFieldBuilder(name, this))); }); } interfaceField>( ref: Type, fieldName: string, field: InterfaceFieldThunk>, ) { verifyRef(ref); this.configStore.onTypeConfig(ref, ({ name }) => { this.configStore.addFields(ref, () => ({ [fieldName]: field(new InterfaceFieldBuilder(name, this)), })); }); } unionType>( name: string, options: GiraphQLSchemaTypes.UnionTypeOptions, ) { const ref = new UnionRef, ParentShape>(name); options.types.forEach((type) => { verifyRef(type); }); const config: GiraphQLUnionTypeConfig = { kind: 'Union', graphqlKind: 'Union', name, types: (options.types || []) as ObjectParam[], description: options.description, resolveType: options.resolveType as GraphQLTypeResolver, giraphqlOptions: options as unknown as GiraphQLSchemaTypes.UnionTypeOptions, extensions: options.extensions, }; this.configStore.addTypeConfig(config, ref); return ref; } enumType>( param: Param, options: EnumTypeOptions, ) { verifyRef(param); const name = typeof param === 'string' ? param : (options as { name: string }).name; const ref = new EnumRef< Param extends BaseEnum ? ValuesFromEnum : ShapeFromEnumValues >(name); const values = typeof param === 'object' ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion valuesFromEnum(param as BaseEnum) : normalizeEnumValues((options as { values: EnumValues }).values); const config: GiraphQLEnumTypeConfig = { kind: 'Enum', graphqlKind: 'Enum', name, values, description: options.description, giraphqlOptions: options as unknown as GiraphQLSchemaTypes.EnumTypeOptions, extensions: options.extensions, }; this.configStore.addTypeConfig(config, ref); if (typeof param !== 'string') { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion this.configStore.associateRefWithName(param as BaseEnum, name); } return ref; } scalarType>( name: Name, options: GiraphQLSchemaTypes.ScalarTypeOptions< Types, InputShape, ParentShape >, ) { const ref = new ScalarRef, ParentShape>(name); const config: GiraphQLScalarTypeConfig = { kind: 'Scalar', graphqlKind: 'Scalar', name, description: options.description, parseLiteral: options.parseLiteral, parseValue: options.parseValue, serialize: options.serialize as GraphQLScalarSerializer>, giraphqlOptions: options as unknown as GiraphQLSchemaTypes.ScalarTypeOptions, extensions: options.extensions, }; this.configStore.addTypeConfig(config, ref); return ref; } addScalarType>( name: Name, scalar: GraphQLScalarType, options: Omit< GiraphQLSchemaTypes.ScalarTypeOptions< Types, InputShape, ParentShape >, 'description' | 'parseLiteral' | 'parseValue' | 'serialize' >, ) { const config = scalar.toConfig(); return this.scalarType(name, { ...config, ...options, } as GiraphQLSchemaTypes.ScalarTypeOptions, ParentShape>); } inputType< Param extends InputObjectRef | string, Fields extends Param extends GiraphQLSchemaTypes.InputObjectRef ? InputFieldsFromShape & {}> : InputFieldMap, >( param: Param, options: GiraphQLSchemaTypes.InputObjectTypeOptions, ): GiraphQLSchemaTypes.InputObjectRef> { verifyRef(param); const name = typeof param === 'string' ? param : (param as { name: string }).name; const ref = ( typeof param === 'string' ? new InputObjectRef>(name) : param ) as GiraphQLSchemaTypes.InputObjectRef>; const config: GiraphQLInputObjectTypeConfig = { kind: 'InputObject', graphqlKind: 'InputObject', name, description: options.description, giraphqlOptions: options as unknown as GiraphQLSchemaTypes.InputObjectTypeOptions, extensions: options.extensions, }; this.configStore.addTypeConfig(config, ref); this.configStore.addFields(ref, () => options.fields(new InputFieldBuilder(this, 'InputObject', name)), ); return ref; } inputRef(name: string): ImplementableInputObjectRef { return new ImplementableInputObjectRef(this, name); } objectRef(name: string): ImplementableObjectRef { return new ImplementableObjectRef(this, name); } interfaceRef(name: string): ImplementableInterfaceRef { return new ImplementableInterfaceRef(this, name); } toSchema(options: GiraphQLSchemaTypes.BuildSchemaOptions) { const { directives, extensions } = options; const scalars = [GraphQLID, GraphQLInt, GraphQLFloat, GraphQLString, GraphQLBoolean]; scalars.forEach((scalar) => { if (!this.configStore.hasConfig(scalar.name as OutputType)) { this.addScalarType(scalar.name as ScalarName, scalar, {}); } }); const buildCache = new BuildCache(this, options); buildCache.plugin.beforeBuild(); buildCache.buildAll(); const builtTypes = [...buildCache.types.values()]; const schema = new GraphQLSchema({ query: buildCache.types.get('Query') as GraphQLObjectType | undefined, mutation: buildCache.types.get('Mutation') as GraphQLObjectType | undefined, subscription: buildCache.types.get('Subscription') as GraphQLObjectType | undefined, extensions, directives: directives as GraphQLDirective[], types: builtTypes, }); return lexicographicSortSchema(buildCache.plugin.afterBuild(schema)); } }