UNPKG

@pothos/core

Version:

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

231 lines (188 loc) 6.66 kB
import type { BuildCache } from '../build-cache'; import { PothosSchemaError } from '../errors'; import type { PothosInputFieldConfig, PothosInputFieldType, PothosTypeConfig, SchemaTypes, } from '../types'; import { unwrapInputFieldType } from './params'; export interface InputTypeFieldsMapping<Types extends SchemaTypes, T> { configs: Record<string, PothosInputFieldConfig<Types>>; map: InputFieldsMapping<Types, T> | null; } export type InputFieldMapping<Types extends SchemaTypes, T> = | { kind: 'Enum'; isList: boolean; config: PothosInputFieldConfig<Types>; value: T; } | { kind: 'InputObject'; config: PothosInputFieldConfig<Types>; isList: boolean; value: T | null; fields: InputTypeFieldsMapping<Types, T>; } | { kind: 'Scalar'; isList: boolean; config: PothosInputFieldConfig<Types>; value: T; }; export type InputFieldsMapping<Types extends SchemaTypes, T> = Map< string, InputFieldMapping<Types, T> >; export function resolveInputTypeConfig<Types extends SchemaTypes>( type: PothosInputFieldType<Types>, buildCache: BuildCache<Types>, ): Extract<PothosTypeConfig, { kind: 'Enum' | 'InputObject' | 'Scalar' }> { if (type.kind === 'List') { return resolveInputTypeConfig(type.type, buildCache); } const config = buildCache.getTypeConfig(type.ref); if (config.kind === 'Enum' || config.kind === 'Scalar' || config.kind === 'InputObject') { return config; } throw new PothosSchemaError( `Unexpected config type ${config.kind} for input ref ${String(type.ref)}`, ); } export function mapInputFields<Types extends SchemaTypes, T>( inputs: Record<string, PothosInputFieldConfig<Types>>, buildCache: BuildCache<Types>, mapper: (config: PothosInputFieldConfig<Types>) => T | null, cache: Map<string, InputTypeFieldsMapping<Types, T>> = new Map(), ): InputFieldsMapping<Types, T> | null { const filterMappings = new Map<InputFieldsMapping<Types, T>, InputFieldsMapping<Types, T>>(); const hasMappings = new Map<InputFieldsMapping<Types, T>, boolean>(); return filterMapped(internalMapInputFields(inputs, buildCache, mapper, cache)); function filterMapped(map: InputFieldsMapping<Types, T>) { if (filterMappings.has(map)) { return filterMappings.get(map)!; } const filtered = new Map<string, InputFieldMapping<Types, T>>(); filterMappings.set(map, filtered); map.forEach((mapping, fieldName) => { if (mapping.kind === 'Enum' || mapping.kind === 'Scalar') { filtered.set(fieldName, mapping); return; } const hasNestedMappings = checkForMappings(mapping.fields.map!, hasMappings); if (mapping.value !== null || hasNestedMappings) { const filteredTypeFields = filterMapped(mapping.fields.map!); const mappingForType = { ...mapping, fields: { configs: mapping.fields.configs, map: filteredTypeFields, }, }; filtered.set(fieldName, mappingForType); } }); return filtered.size > 0 ? filtered : null; } function checkForMappings( map: InputFieldsMapping<Types, T>, hasMappings: Map<InputFieldsMapping<Types, T>, boolean>, ): boolean { if (hasMappings.has(map)) { return hasMappings.get(map)!; } hasMappings.set(map, false); let result = false; for (const mapping of map.values()) { if (mapping.value !== null) { result = true; } else if ( mapping.kind === 'InputObject' && mapping.fields.map && checkForMappings(mapping.fields.map, hasMappings) ) { result = true; } } hasMappings.set(map, result); return result; } } function internalMapInputFields<Types extends SchemaTypes, T>( inputs: Record<string, PothosInputFieldConfig<Types>>, buildCache: BuildCache<Types>, mapper: (config: PothosInputFieldConfig<Types>) => T | null, seenTypes: Map<string, InputTypeFieldsMapping<Types, T>>, ) { const map = new Map<string, InputFieldMapping<Types, T>>(); for (const [fieldName, inputField] of Object.entries(inputs)) { const typeConfig = resolveInputTypeConfig(inputField.type, buildCache); const fieldMapping = mapper(inputField); if (typeConfig.kind === 'Enum' || typeConfig.kind === 'Scalar') { if (fieldMapping !== null) { map.set(fieldName, { kind: typeConfig.kind, isList: inputField.type.kind === 'List', config: inputField, value: fieldMapping, }); } continue; } const inputFieldConfigs = buildCache.getInputTypeFieldConfigs( unwrapInputFieldType(inputField.type), ); if (!seenTypes.has(typeConfig.name)) { const typeEntry = { configs: inputFieldConfigs, map: new Map<string, InputFieldMapping<Types, T>>(), }; seenTypes.set(typeConfig.name, typeEntry); typeEntry.map = internalMapInputFields(inputFieldConfigs, buildCache, mapper, seenTypes); } const typeFields = seenTypes.get(typeConfig.name)!; map.set(fieldName, { kind: typeConfig.kind, isList: inputField.type.kind === 'List', config: inputField, value: fieldMapping, fields: typeFields, }); } return map; } export function createInputValueMapper<Types extends SchemaTypes, T, Args extends unknown[] = []>( argMap: InputFieldsMapping<Types, T>, mapValue: (val: unknown, mapping: InputFieldMapping<Types, T>, ...args: Args) => unknown, ) { return function mapObject( obj: object, map: InputFieldsMapping<Types, T> = argMap, ...args: Args ) { const mapped: Record<string, unknown> = { ...obj }; map.forEach((field, fieldName) => { let fieldVal = (obj as Record<string, unknown>)[fieldName]; if (fieldVal === null || fieldVal === undefined) { return; } if (field.kind === 'InputObject' && field.fields.map) { fieldVal = field.isList ? (fieldVal as (Record<string, unknown> | null)[]).map( (val) => val && mapObject(val, field.fields.map!, ...args), ) : mapObject(fieldVal as Record<string, unknown>, field.fields.map, ...args); mapped[fieldName] = fieldVal; } if (field.kind !== 'InputObject' || field.value !== null) { mapped[fieldName] = field.isList ? (fieldVal as unknown[]).map((val) => val == null ? val : mapValue(val, field, ...args), ) : mapValue(fieldVal, field, ...args); } }); return mapped; }; }