UNPKG

@nestjs/graphql

Version:

Nest - modern, fast, powerful node.js web framework (@graphql)

168 lines (167 loc) 6.23 kB
import { isUndefined } from '@nestjs/common/utils/shared.utils.js'; import { ARGS_TYPE_METADATA } from '../graphql.constants.js'; import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage.js'; const propsCache = new WeakMap(); const hasRenamedPropsCache = new WeakMap(); const mappedPropsCache = new WeakMap(); function getProps(classRef) { if (propsCache.has(classRef)) { return propsCache.get(classRef); } const classesMetadata = [ ...TypeMetadataStorage.getArgumentsMetadata(), ...TypeMetadataStorage.getInputTypesMetadata(), ]; const properties = []; let target = classRef; while (!isUndefined(target?.prototype)) { const classMetadata = classesMetadata.find((item) => item.target === target); if (classMetadata?.properties) { properties.push(...classMetadata.properties); } target = Object.getPrototypeOf(target); } propsCache.set(classRef, properties); return properties; } function hasRenamedProps(classRef) { if (hasRenamedPropsCache.has(classRef)) { return hasRenamedPropsCache.get(classRef); } // iterative instead of recursive, input types are allowed to reference each other const visited = new Set(); const queue = [classRef]; let hasRenamed = false; while (queue.length && !hasRenamed) { const current = queue.pop(); if (visited.has(current)) { continue; } visited.add(current); for (const { name, schemaName, typeFn } of getProps(current)) { if (schemaName !== name) { hasRenamed = true; break; } const typeRef = typeFn?.(); if (typeof typeRef === 'function') { queue.push(typeRef); } } } hasRenamedPropsCache.set(classRef, hasRenamed); return hasRenamed; } function getMappedProps(classRef) { if (mappedPropsCache.has(classRef)) { return mappedPropsCache.get(classRef); } const mappedProps = []; getProps(classRef).forEach(({ name, schemaName, typeFn }) => { const typeRef = typeFn?.(); const nestedTypeRef = typeof typeRef === 'function' && hasRenamedProps(typeRef) ? typeRef : undefined; if (schemaName !== name || nestedTypeRef) { mappedProps.push({ schemaName, name, typeRef: nestedTypeRef }); } }); mappedPropsCache.set(classRef, mappedProps); return mappedProps; } /** * Rewrites `value` so that every field renamed through `@Field({ name: '...' })` * is keyed by the property name its class declares instead of its schema name. * * The result is always a fresh object: reading the renamed fields from the * original `value` keeps chained renames (a property whose schema name is * another property's class name) from overwriting each other. */ export function mapArgsToProps(value, classRef) { const mappedProps = getMappedProps(classRef); if (!mappedProps.length || !value || typeof value !== 'object') { return value; } if (Array.isArray(value)) { return value.map((item) => mapArgsToProps(item, classRef)); } const renamed = new Set(mappedProps .filter(({ schemaName, name }) => schemaName !== name) .map(({ schemaName }) => schemaName)); const mapped = {}; for (const key of Object.keys(value)) { if (!renamed.has(key)) { mapped[key] = value[key]; } } for (const { schemaName, name, typeRef } of mappedProps) { if (!(schemaName in value)) { continue; } mapped[name] = typeRef ? mapArgsToProps(value[schemaName], typeRef) : value[schemaName]; } return mapped; } function resolveArgClass({ index, typeFn }, target, methodName) { // The explicit `type` option wins: a list argument reflects as `Array`, which // tells us nothing about the element type. let typeRef = typeFn?.(); while (Array.isArray(typeRef)) { typeRef = typeRef[0]; } if (!typeRef) { const paramtypes = Reflect.getMetadata('design:paramtypes', target.prototype, methodName) ?? []; typeRef = paramtypes[index]; } return typeof typeRef === 'function' ? typeRef : undefined; } /** * Builds the function that maps the raw GraphQL arguments of a single resolver * method onto the property names its `@Args()` classes declare, or `null` when * the method takes no arguments at all. * * The classes involved are resolved lazily, on the first call, because the * metadata this relies on is only complete once the schema has been built. */ export function createArgsMapper(target, methodName) { const argsTypes = Reflect.getMetadata(ARGS_TYPE_METADATA, target, methodName); if (!argsTypes?.length) { return null; } let entries; return (args) => { if (!entries) { entries = argsTypes .map((argsType) => ({ property: argsType.property, classRef: resolveArgClass(argsType, target, methodName), })) .filter(({ classRef }) => classRef && hasRenamedProps(classRef)); // Named arguments read from the incoming, schema-keyed object, so they // have to be mapped before a whole-object `@Args()` renames their keys. entries.sort((a, b) => Number(!a.property) - Number(!b.property)); } if (!entries.length || !args || typeof args !== 'object') { return args; } let result = args; for (const { property, classRef } of entries) { if (!property) { result = mapArgsToProps(result, classRef); continue; } if (!(property in result)) { continue; } const mappedValue = mapArgsToProps(result[property], classRef); if (mappedValue !== result[property]) { result = result === args ? { ...result } : result; result[property] = mappedValue; } } return result; }; }