/* eslint-disable node/no-callback-literal */ import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLScalarType, GraphQLString, } from 'graphql'; import { FieldMap, InputRef, OutputRef, SchemaTypes } from './types'; import { BaseTypeRef, BuiltinScalarRef, ConfigurableRef, FieldRef, GiraphQLFieldConfig, GiraphQLObjectTypeConfig, GiraphQLTypeConfig, GraphQLFieldKind, InputFieldMap, InputFieldRef, InputType, InputTypeParam, InputTypeRef, OutputType, OutputTypeRef, TypeParam, } from '.'; export default class ConfigStore { typeConfigs = new Map(); private fieldRefs = new WeakMap< FieldRef | InputFieldRef, (name: string, parentField: string | undefined) => GiraphQLFieldConfig >(); private fields = new Map>>(); private addFieldFns: (() => void)[] = []; private refsToName = new Map, string>(); private scalarsToRefs = new Map>(); private fieldRefsToConfigs = new Map[]>(); private pendingFields = new Map | OutputType>(); private pendingRefResolutions = new Map< ConfigurableRef, ((config: GiraphQLTypeConfig) => void)[] >(); private fieldRefCallbacks = new Map< FieldRef | InputFieldRef, ((config: GiraphQLFieldConfig) => void)[] >(); private pending = true; constructor() { const scalars: GraphQLScalarType[] = [ GraphQLID, GraphQLInt, GraphQLFloat, GraphQLString, GraphQLBoolean, ]; scalars.forEach((scalar) => { const ref = new BuiltinScalarRef(scalar); this.scalarsToRefs.set(scalar.name, ref); this.refsToName.set(ref, scalar.name); }); } hasConfig(typeParam: InputType | OutputType) { if (typeof typeParam === 'string') { return this.typeConfigs.has(typeParam); } return this.refsToName.has(typeParam); } addFieldRef( ref: FieldRef | InputFieldRef, // We need to be able to resolve the types kind before configuring the field typeParam: InputTypeParam | TypeParam, args: InputFieldMap, getConfig: (name: string, parentField: string | undefined) => GiraphQLFieldConfig, ) { if (this.fieldRefs.has(ref)) { throw new Error(`FieldRef ${String(ref)} has already been added to config store`); } const typeRefOrName = Array.isArray(typeParam) ? typeParam[0] : typeParam; const argRefs = Object.keys(args).map((argName) => { const argRef = args[argName]; argRef.fieldName = argName; argRef.argFor = ref; return argRef; }); const checkArgs = () => { for (const arg of argRefs) { if (this.pendingFields.has(arg)) { const unresolvedArgType = this.pendingFields.get(arg)!; this.pendingFields.set(ref, unresolvedArgType); this.onTypeConfig(unresolvedArgType, checkArgs); return; } } this.pendingFields.delete(ref); this.fieldRefs.set(ref, getConfig); }; if ( this.hasConfig(typeRefOrName) || typeRefOrName instanceof BaseTypeRef || this.scalarsToRefs.has(typeRefOrName as string) ) { checkArgs(); } else { this.pendingFields.set(ref, typeRefOrName); this.onTypeConfig(typeRefOrName, () => { checkArgs(); }); } } createFieldConfig( ref: FieldRef | InputFieldRef, name: string, parentField?: string, kind?: T, ): Extract, { graphqlKind: T }> { if (!this.fieldRefs.has(ref)) { if (this.pendingFields.has(ref)) { throw new Error( `Missing implementation for ${this.describeRef(this.pendingFields.get(ref)!)}`, ); } throw new Error(`Missing definition for for ${String(ref)}`); } const config = this.fieldRefs.get(ref)!(name, parentField); if (kind && config.graphqlKind !== kind) { throw new TypeError( `Expected ref for field named ${name} to resolve to a ${kind} type, but got ${config.graphqlKind}`, ); } return config as Extract, { graphqlKind: T }>; } associateRefWithName(ref: ConfigurableRef, name: string) { if (!this.typeConfigs.has(name)) { throw new Error(`${name} has not been implemented yet`); } this.refsToName.set(ref, name); if (this.pendingRefResolutions.has(ref)) { const cbs = this.pendingRefResolutions.get(ref)!; this.pendingRefResolutions.delete(ref); cbs.forEach((cb) => void cb(this.typeConfigs.get(name)!)); } } addTypeConfig(config: GiraphQLTypeConfig, ref?: ConfigurableRef) { const { name } = config; if (this.typeConfigs.has(name)) { throw new Error(`Duplicate typename: Another type with name ${name} already exists.`); } this.typeConfigs.set(config.name, config); if (ref) { this.associateRefWithName(ref, name); } if (this.pendingRefResolutions.has(name as ConfigurableRef)) { const cbs = this.pendingRefResolutions.get(name as ConfigurableRef)!; this.pendingRefResolutions.delete(name as ConfigurableRef); cbs.forEach((cb) => void cb(config)); } } getTypeConfig( ref: ConfigurableRef | string, kind?: T, ) { let config: GiraphQLTypeConfig; if (typeof ref === 'string') { if (!this.typeConfigs.has(ref)) { throw new Error(`Type ${String(ref)} has not been implemented`); } config = this.typeConfigs.get(ref)!; } else if (this.refsToName.has(ref)) { config = this.typeConfigs.get(this.refsToName.get(ref)!)!; } else { throw new Error(`Ref ${String(ref)} has not been implemented`); } if (kind && config.graphqlKind !== kind) { throw new TypeError(`Expected ref to resolve to a ${kind} type, but got ${config.kind}`); } return config as Extract; } getInputTypeRef(ref: ConfigurableRef | string) { if (ref instanceof BaseTypeRef) { if (ref.kind !== 'InputObject' && ref.kind !== 'Enum' && ref.kind !== 'Scalar') { throw new TypeError(`Expected ${ref.name} to be an input type but got ${ref.kind}`); } return ref as InputRef; } if (typeof ref === 'string') { if (this.scalarsToRefs.has(ref)) { return this.scalarsToRefs.get(ref)!; } if (this.typeConfigs.has(ref)) { const config = this.typeConfigs.get(ref)!; if ( config.graphqlKind !== 'InputObject' && config.graphqlKind !== 'Enum' && config.graphqlKind !== 'Scalar' ) { throw new TypeError( `Expected ${config.name} to be an input type but got ${config.graphqlKind}`, ); } const newRef = new InputTypeRef(config.graphqlKind, config.name); this.refsToName.set(newRef, config.name); return newRef; } } return ref as InputType; } getOutputTypeRef(ref: ConfigurableRef | string) { if (ref instanceof BaseTypeRef) { if (ref.kind === 'InputObject') { throw new TypeError(`Expected ${ref.name} to be an output type but got ${ref.name}`); } return ref as OutputRef; } if (typeof ref === 'string') { if (this.scalarsToRefs.has(ref)) { return this.scalarsToRefs.get(ref)!; } if (this.typeConfigs.has(ref)) { const config = this.typeConfigs.get(ref)!; if (config.graphqlKind === 'InputObject') { throw new TypeError( `Expected ${config.name} to be an output type but got ${config.graphqlKind}`, ); } const newRef = new OutputTypeRef(config.graphqlKind, config.name); this.refsToName.set(newRef, config.name); return newRef; } } return ref as OutputType; } onTypeConfig(ref: ConfigurableRef, cb: (config: GiraphQLTypeConfig) => void) { if (!ref) { throw new Error(`${ref} is not a valid type ref`); } if (this.refsToName.has(ref)) { cb(this.getTypeConfig(ref)); } else if (typeof ref === 'string' && this.typeConfigs.has(ref)) { cb(this.typeConfigs.get(ref)!); } else if (!this.pending) { throw new Error(`Ref ${String(ref)} has not been implemented`); } else if (this.pendingRefResolutions.has(ref)) { this.pendingRefResolutions.get(ref)!.push(cb); } else { this.pendingRefResolutions.set(ref, [cb]); } } onFieldUse(ref: FieldRef | InputFieldRef, cb: (config: GiraphQLFieldConfig) => void) { if (!this.fieldRefCallbacks.has(ref)) { this.fieldRefCallbacks.set(ref, []); } this.fieldRefCallbacks.get(ref)!.push(cb); if (this.fieldRefsToConfigs.has(ref)) { this.fieldRefsToConfigs.get(ref)!.forEach((config) => void cb(config)); } } getFields( name: string, kind?: T, ): Record, { graphqlKind: T }>> { const typeConfig = this.getTypeConfig(name); const fields = this.fields.get(name) ?? []; if (kind && typeConfig.graphqlKind !== kind) { throw new TypeError( `Expected ${name} to be a ${kind} type, but found ${typeConfig.graphqlKind}`, ); } return fields as Record, { graphqlKind: T }>>; } prepareForBuild() { this.pending = false; const fns = this.addFieldFns; this.addFieldFns = []; fns.forEach((fn) => void fn()); if (this.pendingRefResolutions.size > 0) { throw new Error( `Missing implementations for some references (${[...this.pendingRefResolutions.keys()] .map((ref) => this.describeRef(ref)) .join(', ')}).`, ); } } addFields( typeRef: ConfigurableRef, fields: FieldMap | InputFieldMap | (() => FieldMap | InputFieldMap), ) { if (this.pending) { this.addFieldFns.push(() => void this.addFields(typeRef, fields)); } else { this.onTypeConfig(typeRef, (config) => { this.buildFields(typeRef, typeof fields === 'function' ? fields() : fields); }); } } getImplementers(ref: ConfigurableRef | string) { const typeConfig = this.getTypeConfig(ref, 'Interface'); const implementers = [...this.typeConfigs.values()].filter( (type) => type.kind === 'Object' && type.interfaces.find((i) => this.getTypeConfig(i).name === typeConfig.name), ) as GiraphQLObjectTypeConfig[]; return implementers; } private describeRef(ref: ConfigurableRef): string { if (typeof ref === 'string') { return ref; } if (ref.toString !== {}.toString) { return String(ref); } const usedBy = [...this.pendingFields.entries()].find( ([fieldRef, typeRef]) => typeRef === ref, )?.[0]; if (usedBy) { return ``; } return ``; } private buildFields(typeRef: ConfigurableRef, fields: FieldMap | InputFieldMap) { const typeConfig = this.getTypeConfig(typeRef); if (!this.fields.has(typeConfig.name)) { this.fields.set(typeConfig.name, {}); } Object.keys(fields).forEach((fieldName) => { const fieldRef = fields[fieldName]; fieldRef.fieldName = fieldName; if (this.pendingFields.has(fieldRef)) { this.onTypeConfig(this.pendingFields.get(fieldRef)!, () => { this.buildField(typeRef, fieldRef, fieldName); }); } else { this.buildField(typeRef, fieldRef, fieldName); } }); } private buildField( typeRef: ConfigurableRef, field: FieldRef | InputFieldRef, fieldName: string, ) { const typeConfig = this.getTypeConfig(typeRef); const fieldConfig = this.createFieldConfig(field, fieldName); const existingFields = this.fields.get(typeConfig.name)!; if (existingFields[fieldName]) { throw new Error(`Duplicate field definition for field ${fieldName} in ${typeConfig.name}`); } if (fieldConfig.graphqlKind !== typeConfig.graphqlKind) { throw new TypeError( `${typeConfig.name}.${fieldName} was defined as a ${fieldConfig.graphqlKind} field but ${typeConfig.name} is a ${typeConfig.graphqlKind}`, ); } existingFields[fieldName] = fieldConfig; if (!this.fieldRefsToConfigs.has(field)) { this.fieldRefsToConfigs.set(field, []); } this.fieldRefsToConfigs.get(field)!.push(fieldConfig); if (this.fieldRefCallbacks.has(field)) { this.fieldRefCallbacks.get(field)!.forEach((cb) => void cb(fieldConfig)); } } }