import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { FragmentDefinitionNode, SelectionNode } from 'graphql'; import { BaseEntity } from './common'; import { extractFieldsData } from './field-extractor'; interface SelectedFieldsDecoratorParams { sqlAlias?: string; nestedPolymorphicResolverName?: string; } export interface SelectedUnionTypesResult { has: (key) => boolean; getFields: (key) => string[]; types: Map; } export const SelectedUnionTypes = createParamDecorator( (data: SelectedFieldsDecoratorParams, ctx: ExecutionContext) => { const args = ctx.getArgs(); const info = args[3]; return getSelectedUnionTypes(info, data); }, ); export function getSelectedUnionTypes( info, options?: SelectedFieldsDecoratorParams, ) { const returnObj: SelectedUnionTypesResult = { has(entity: BaseEntity | string) { if (typeof entity === 'string') { return this['types'].has(entity); } else { return this['types'].has((entity as BaseEntity).name); } }, getFields(entity: BaseEntity | string) { let fields = []; if (typeof entity === 'string') { fields = this['types'].get(entity); } else { fields = this['types'].get((entity as BaseEntity).name); } return fields.map((field) => options?.sqlAlias ? `${options?.sqlAlias}.${field}` : field, ); }, types: null, }; // To avoid multiple calls when using dataloader define getter field to be able to compute only once inside the loader function Object.defineProperty(returnObj, 'types', { get: () => { const rowFieldsData = extractUnionsData( info.fieldNodes, options?.nestedPolymorphicResolverName || info.fieldName, info.fragments, ); return rowFieldsData; }, }); return returnObj; } export function extractUnionsData( resolvers: ReadonlyArray, field: string, fragments: { [key: string]: FragmentDefinitionNode }, ) { const results = new Map([]); function process( resolvers: ReadonlyArray, field: string, fragments: { [key: string]: FragmentDefinitionNode }, ) { for (const resolver of resolvers) { if (resolver.kind === 'Field' && resolver.selectionSet) { const unifiedName = resolver.name.value; if (unifiedName === field) { resolver.selectionSet.selections.forEach((item) => { if (item.kind === 'Field') { return; } else if (item.kind === 'FragmentSpread') { const fragment = fragments[item.name.value]; if (fragment?.selectionSet) { process(fragment.selectionSet.selections, field, fragments); } } else if (item.kind === 'InlineFragment') { const data = extractFieldsData( item.selectionSet.selections, item.typeCondition.name.value, fragments, true, ); results.set(item.typeCondition.name.value, [...data]); } }); return results; } else { return process(resolver.selectionSet.selections, field, fragments); } } } return results; } process(resolvers, field, fragments); return results; }