import type { BuildCache } from '../build-cache.js'; import { PothosSchemaError } from '../errors.js'; import type { PothosInputFieldConfig, PothosInputFieldType, PothosTypeConfig, SchemaTypes, } from '../types/index.js'; import { unwrapInputFieldType } from './params.js'; export interface InputTypeFieldsMapping { configs: Record>; map: InputFieldsMapping | null; } export type InputFieldMapping = | { kind: 'Enum'; isList: boolean; listDepth: number; config: PothosInputFieldConfig; value: T; } | { kind: 'InputObject'; config: PothosInputFieldConfig; isList: boolean; listDepth: number; value: T | null; fields: InputTypeFieldsMapping; } | { kind: 'Scalar'; isList: boolean; listDepth: number; config: PothosInputFieldConfig; value: T; }; export type InputFieldsMapping = Map< string, InputFieldMapping >; export function resolveInputTypeConfig( type: PothosInputFieldType, buildCache: BuildCache, ): Extract { 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( inputs: Record>, buildCache: BuildCache, mapper: (config: PothosInputFieldConfig) => T | null, cache: Map> = new Map(), ): InputFieldsMapping | null { const filterMappings = new Map, InputFieldsMapping>(); const hasMappings = new Map, boolean>(); return filterMapped(internalMapInputFields(inputs, buildCache, mapper, cache)); function filterMapped(map: InputFieldsMapping) { if (filterMappings.has(map)) { return filterMappings.get(map)!; } const filtered = new Map>(); filterMappings.set(map, filtered); map.forEach((mapping, fieldName) => { if (mapping.kind === 'Enum' || mapping.kind === 'Scalar') { filtered.set(fieldName, mapping); return; } const hasNestedMappings = mapping.fields.map ? checkForMappings(mapping.fields.map) : false; if (mapping.value !== null || hasNestedMappings) { const filteredTypeFields = mapping.fields.map ? filterMapped(mapping.fields.map) : null; const mappingForType = { ...mapping, fields: { configs: mapping.fields.configs, map: filteredTypeFields, }, }; filtered.set(fieldName, mappingForType); } }); return filtered.size > 0 ? filtered : null; } function checkForMappings(map: InputFieldsMapping): boolean { const openDepths = new Map, number>(); const pending: InputFieldsMapping[] = []; // Tarjan's SCC rule: `openDepth` is the shallowest still-open map this subtree reaches, and a // map commits only once nothing beneath it reaches shallower. Everything spliced off `pending` // is then one cycle, so the single answer computed for it holds for every member. function visit(map: InputFieldsMapping): { result: boolean; openDepth: number; } { if (hasMappings.has(map)) { return { result: hasMappings.get(map)!, openDepth: Number.POSITIVE_INFINITY }; } if (openDepths.has(map)) { return { result: false, openDepth: openDepths.get(map)! }; } const depth = pending.length; openDepths.set(map, depth); pending.push(map); let result = false; let openDepth = Number.POSITIVE_INFINITY; for (const mapping of map.values()) { if (mapping.value !== null) { result = true; } else if (mapping.kind === 'InputObject' && mapping.fields.map) { const nested = visit(mapping.fields.map); result ||= nested.result; openDepth = Math.min(openDepth, nested.openDepth); } } if (openDepth < depth) { return { result, openDepth }; } for (const resolved of pending.splice(depth)) { openDepths.delete(resolved); hasMappings.set(resolved, result); } return { result, openDepth: Number.POSITIVE_INFINITY }; } return visit(map).result; } } function internalMapInputFields( inputs: Record>, buildCache: BuildCache, mapper: (config: PothosInputFieldConfig) => T | null, seenTypes: Map>, ) { const map = new Map>(); 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', listDepth: getListDepth(inputField.type), config: inputField, value: fieldMapping, }); } continue; } const inputFieldConfigs = buildCache.getInputTypeFieldConfigs( unwrapInputFieldType(inputField.type), ); if (!seenTypes.has(typeConfig.name)) { const typeEntry = { configs: inputFieldConfigs, map: new Map>(), }; 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', listDepth: getListDepth(inputField.type), config: inputField, value: fieldMapping, fields: typeFields, }); } return map; } export function createInputValueMapper( argMap: InputFieldsMapping, mapValue: (val: unknown, mapping: InputFieldMapping, ...args: Args) => unknown, ) { return function mapObject( obj: object, map: InputFieldsMapping = argMap, ...args: Args ) { const mapped: Record = { ...obj }; map.forEach((field, fieldName) => { let fieldVal = (obj as Record)[fieldName]; if (fieldVal === null || fieldVal === undefined) { return; } if (field.kind === 'InputObject' && field.fields.map) { fieldVal = mapListValue( fieldVal, field.listDepth, (val) => val && mapObject(val, field.fields.map!, ...args), ); mapped[fieldName] = fieldVal; } if (field.kind !== 'InputObject' || field.value !== null) { mapped[fieldName] = mapListValue(fieldVal, field.listDepth, (val) => val == null ? val : mapValue(val, field, ...args), ); } }); return mapped; }; } function getListDepth(type: PothosInputFieldType): number { let depth = 0; let current = type; while (current.kind === 'List') { depth++; current = current.type; } return depth; } function mapListValue( value: unknown, listDepth: number, mapper: (val: unknown) => unknown, ): unknown { if (listDepth === 0) { return mapper(value); } if (!Array.isArray(value)) { return value; } return value.map((item) => listDepth > 1 ? mapListValue(item, listDepth - 1, mapper) : mapper(item), ); }