import { PothosSchemaError } from './errors'; import { BaseTypeRef } from './refs/base'; import { InputObjectRef } from './refs/input-object'; import { InterfaceRef } from './refs/interface'; import { MutationRef } from './refs/mutation'; import { ObjectRef } from './refs/object'; import { QueryRef } from './refs/query'; import { SubscriptionRef } from './refs/subscription'; import type { ConfigurableRef, FieldMap, GraphQLFieldKind, InputFieldMap, InputRef, OutputType, PothosFieldConfig, PothosTypeConfig, SchemaTypes, } from './types'; export class ConfigStore { typeConfigs = new Map(); private fields = new Map>>(); private refs = new Set>(); private implementors = new Map>(); private pendingActions: (() => void)[] = []; private paramAssociations = new Map(); private pendingTypeConfigResolutions = new Map< unknown, ((config: PothosTypeConfig, ref: BaseTypeRef) => void)[] >(); private pending = true; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: backwards compatibility private builder: PothosSchemaTypes.SchemaBuilder; constructor(builder: PothosSchemaTypes.SchemaBuilder) { this.builder = builder; } addFields(param: ConfigurableRef, fields: () => FieldMap) { this.onTypeConfig(param, (_config, ref) => { if ( !( ref instanceof InterfaceRef || ref instanceof ObjectRef || ref instanceof QueryRef || ref instanceof MutationRef || ref instanceof SubscriptionRef ) ) { throw new PothosSchemaError(`Can not add fields to ${ref} because it is not an object`); } ref.addFields(fields); }); } addInputFields(param: ConfigurableRef, fields: () => InputFieldMap) { this.onTypeConfig(param, (_config, ref) => { if (!(ref instanceof InputObjectRef)) { throw new PothosSchemaError( `Can not add fields to ${ref} because it is not an input object`, ); } ref.addFields(fields); }); } associateParamWithRef(param: ConfigurableRef, ref: BaseTypeRef | string) { const resolved = this.resolveParamAssociations(ref); this.paramAssociations.set(param, resolved); const pendingResolutions = this.pendingTypeConfigResolutions.get(param) ?? []; if (pendingResolutions.length > 0) { if (typeof resolved === 'string' && this.typeConfigs.has(resolved)) { for (const cb of pendingResolutions) { const config = this.typeConfigs.get(resolved)!; cb(config, this.implementors.get(config.name)!); } } else { for (const cb of pendingResolutions) { this.onTypeConfig(resolved as ConfigurableRef, cb); } } } this.pendingTypeConfigResolutions.delete(param); } onTypeConfig( param: ConfigurableRef, onConfig: (config: PothosTypeConfig, ref: BaseTypeRef) => void, ) { const resolved = this.resolveParamAssociations(param); if (typeof resolved === 'string' && this.typeConfigs.has(resolved)) { const config = this.typeConfigs.get(resolved)!; onConfig(config, this.implementors.get(config.name)!); } else { if (!this.pendingTypeConfigResolutions.has(resolved)) { this.pendingTypeConfigResolutions.set(resolved, []); } this.pendingTypeConfigResolutions.get(resolved)!.push(onConfig); } } onTypeConfigOfKind( param: ConfigurableRef, kind: Kind, onConfig: (config: PothosTypeConfig & { kind: Kind }) => void, ) { this.onTypeConfig(param, (config) => { if (config.kind !== kind) { throw new PothosSchemaError( `Expected ${this.describeRef(param)} to be of kind ${kind} but it is of kind ${ config.kind }`, ); } onConfig(config as PothosTypeConfig & { kind: Kind }); }); } addTypeRef(ref: BaseTypeRef) { if (this.refs.has(ref as BaseTypeRef)) { return; } if (!this.pending) { ref.prepareForBuild(); } this.refs.add(ref as BaseTypeRef); ref.onConfig((config) => { const implementor = this.implementors.get(config.name); if (implementor && implementor !== ref) { throw new PothosSchemaError( `Duplicate typename: Another type with name ${config.name} already exists.`, ); } if (!implementor) { this.implementors.set(config.name, ref as BaseTypeRef); this.associateParamWithRef(ref as BaseTypeRef, config.name); if ( ref instanceof ObjectRef || ref instanceof InterfaceRef || ref instanceof InputObjectRef ) { if (!this.fields.has(config.name)) { this.fields.set(config.name, new Map()); } this.onPrepare(() => { ( ref as | InputObjectRef | InterfaceRef | ObjectRef ).onField((fieldName, field) => { const fields = this.fields.get(config.name)!; if (fields.has(fieldName)) { throw new PothosSchemaError(`Duplicate field ${fieldName} on ${config.name}`); } fields.set( fieldName, field.getConfig(fieldName, this.typeConfigs.get(config.name) ?? config), ); }); }); } } this.typeConfigs.set(config.name, config); if (this.pendingTypeConfigResolutions.has(config.name)) { const cbs = this.pendingTypeConfigResolutions.get(config.name)!; for (const cb of cbs) { cb(config, ref as BaseTypeRef); } } this.pendingTypeConfigResolutions.delete(config.name); }); } subscribeToFields(_ref: BaseTypeRef) {} hasImplementation(typeName: string) { return this.typeConfigs.has(typeName); } hasConfig(ref: ConfigurableRef | string) { const resolved = this.resolveParamAssociations(ref); if (typeof resolved !== 'string' || !this.typeConfigs.has(resolved)) { return false; } return true; } getTypeConfig( ref: ConfigurableRef | string, kind?: T, ) { const resolved = this.resolveParamAssociations(ref); if (typeof resolved !== 'string' || !this.typeConfigs.has(resolved)) { throw new PothosSchemaError(`${this.describeRef(ref)} has not been implemented`); } const config = this.typeConfigs.get(resolved)!; if (kind && config.graphqlKind !== kind) { throw new PothosSchemaError( `Expected ref to resolve to a ${kind} type, but got ${config.kind}`, ); } return config as Extract; } getInputTypeRef(param: ConfigurableRef | string) { const resolved = this.resolveParamAssociations(param); if (param instanceof BaseTypeRef) { if (param.kind !== 'InputObject' && param.kind !== 'Enum' && param.kind !== 'Scalar') { throw new PothosSchemaError( `Expected ${this.describeRef(param)} to be an input type but got ${param.kind}`, ); } return param as unknown as InputRef; } if (typeof resolved === 'string' && this.typeConfigs.has(resolved)) { const ref = this.implementors.get(resolved)!; if (ref instanceof BaseTypeRef) { if (ref.kind !== 'InputObject' && ref.kind !== 'Enum' && ref.kind !== 'Scalar') { throw new PothosSchemaError( `Expected ${this.describeRef(ref)} to be an input type but got ${ref.kind}`, ); } return ref as unknown as InputRef; } } throw new PothosSchemaError(`${this.describeRef(param)} has not been implemented`); } getOutputTypeRef(param: ConfigurableRef | string) { const resolved = this.resolveParamAssociations(param); if (param instanceof BaseTypeRef) { if (param.kind === 'InputObject' || param.kind === 'InputList') { throw new PothosSchemaError( `Expected ${param.name} to be an output type but got ${param.kind}`, ); } return param as unknown as OutputType; } if (typeof resolved === 'string' && this.typeConfigs.has(resolved)) { const ref = this.implementors.get(resolved)!; if (ref instanceof BaseTypeRef) { if (ref.kind === 'InputObject' || ref.kind === 'InputList') { throw new PothosSchemaError( `Expected ${ref.name} to be an output type but got ${ref.kind}`, ); } return ref as unknown as OutputType; } } throw new PothosSchemaError(`${this.describeRef(param)} has not been implemented`); } getFields( name: string, kind?: T, ): Map, { graphqlKind: T }>> { const typeConfig = this.getTypeConfig(name); if (!this.fields.has(name)) { this.fields.set(name, new Map()); } const fields = this.fields.get(name)!; if (kind && typeConfig.graphqlKind !== kind) { throw new PothosSchemaError( `Expected ${name} to be a ${kind} type, but found ${typeConfig.graphqlKind}`, ); } return fields as Map, { graphqlKind: T }>>; } prepareForBuild() { this.pending = false; for (const ref of this.refs) { ref.prepareForBuild(); } const { pendingActions } = this; this.pendingActions = []; for (const fn of pendingActions) { fn(); } if (this.pendingTypeConfigResolutions.size > 0) { throw new PothosSchemaError( `Missing implementations for some references (${[ ...this.pendingTypeConfigResolutions.keys(), ] .map((ref) => this.describeRef(ref as ConfigurableRef)) .join(', ')}).`, ); } } onPrepare(cb: () => void) { if (this.pending) { this.pendingActions.push(cb); } else { cb(); } } private resolveParamAssociations(param: unknown) { let current = this.paramAssociations.get(param); while (current && this.paramAssociations.has(current)) { current = this.paramAssociations.get(current)!; } return current ?? param; } private describeRef(ref: unknown): string { if (typeof ref === 'string') { return ref; } if (ref && ref.toString !== {}.toString) { return String(ref); } if (typeof ref === 'function' && ref.name !== (() => {}).name) { return `function ${ref.name}`; } return ''; } }