@pothos/core
Version:
Pothos (formerly GiraphQL) is a plugin based schema builder for creating code-first GraphQL schemas in typescript
380 lines (302 loc) • 11.1 kB
text/typescript
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<Types extends SchemaTypes> {
typeConfigs = new Map<string, PothosTypeConfig>();
private fields = new Map<string, Map<string, PothosFieldConfig<Types>>>();
private refs = new Set<BaseTypeRef<Types>>();
private implementors = new Map<string, BaseTypeRef<Types>>();
private pendingActions: (() => void)[] = [];
private paramAssociations = new Map<unknown, unknown>();
private pendingTypeConfigResolutions = new Map<
unknown,
((config: PothosTypeConfig, ref: BaseTypeRef<Types>) => void)[]
>();
private pending = true;
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: backwards compatibility
private builder: PothosSchemaTypes.SchemaBuilder<Types>;
constructor(builder: PothosSchemaTypes.SchemaBuilder<Types>) {
this.builder = builder;
}
addFields(param: ConfigurableRef<Types>, 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<Types>, 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<T>(param: ConfigurableRef<Types>, ref: BaseTypeRef<Types, T> | 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<Types>, cb);
}
}
}
this.pendingTypeConfigResolutions.delete(param);
}
onTypeConfig(
param: ConfigurableRef<Types>,
onConfig: (config: PothosTypeConfig, ref: BaseTypeRef<Types>) => 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<Kind extends PothosTypeConfig['kind']>(
param: ConfigurableRef<Types>,
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<T extends PothosTypeConfig>(ref: BaseTypeRef<Types, T>) {
if (this.refs.has(ref as BaseTypeRef<Types>)) {
return;
}
if (!this.pending) {
ref.prepareForBuild();
}
this.refs.add(ref as BaseTypeRef<Types>);
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<Types>);
this.associateParamWithRef(ref as BaseTypeRef<Types>, 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<Types, unknown>
| InterfaceRef<Types, unknown>
| ObjectRef<Types, unknown>
).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<Types>);
}
}
this.pendingTypeConfigResolutions.delete(config.name);
});
}
subscribeToFields(_ref: BaseTypeRef<Types>) {}
hasImplementation(typeName: string) {
return this.typeConfigs.has(typeName);
}
hasConfig(ref: ConfigurableRef<Types> | string) {
const resolved = this.resolveParamAssociations(ref);
if (typeof resolved !== 'string' || !this.typeConfigs.has(resolved)) {
return false;
}
return true;
}
getTypeConfig<T extends PothosTypeConfig['kind']>(
ref: ConfigurableRef<Types> | 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<PothosTypeConfig, { kind: T }>;
}
getInputTypeRef(param: ConfigurableRef<Types> | 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<Types> | 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<Types>;
}
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<Types>;
}
}
throw new PothosSchemaError(`${this.describeRef(param)} has not been implemented`);
}
getFields<T extends GraphQLFieldKind>(
name: string,
kind?: T,
): Map<string, Extract<PothosFieldConfig<Types>, { 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<string, Extract<PothosFieldConfig<Types>, { 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<Types>))
.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 '<unnamed ref or enum>';
}
}