From bcdbba37c53b6301e06f80d90cf14cd288a27cd0 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 13 Jul 2021 22:16:14 +0300 Subject: [PATCH] refactor(stitchingInfo): shift more calculation to build time (#3199) -- run collectFields on selectionSet hints at build time -- use a fieldNode cache at build time so that at run time we can collect unique fieldNodes simply by using a Set --- packages/delegate/src/delegationBindings.ts | 4 +- .../delegate/src/getFieldsNotInSubschema.ts | 42 ++-- .../src/transforms/AddSelectionSets.ts | 43 ++-- packages/delegate/src/types.ts | 4 +- packages/stitch/src/stitchingInfo.ts | 217 +++++++++++------- 5 files changed, 179 insertions(+), 131 deletions(-) diff --git a/packages/delegate/src/delegationBindings.ts b/packages/delegate/src/delegationBindings.ts index 1eb05edc5a9..8d22b761682 100644 --- a/packages/delegate/src/delegationBindings.ts +++ b/packages/delegate/src/delegationBindings.ts @@ -21,8 +21,8 @@ export function defaultDelegationBinding( delegationTransforms = delegationTransforms.concat([ new ExpandAbstractTypes(), new AddSelectionSets( - stitchingInfo.selectionSetsByType, - stitchingInfo.selectionSetsByField, + stitchingInfo.fieldNodesByType, + stitchingInfo.fieldNodesByField, stitchingInfo.dynamicSelectionSetsByField ), new WrapConcreteTypes(), diff --git a/packages/delegate/src/getFieldsNotInSubschema.ts b/packages/delegate/src/getFieldsNotInSubschema.ts index 92b01eec8bc..33a1678f7f1 100644 --- a/packages/delegate/src/getFieldsNotInSubschema.ts +++ b/packages/delegate/src/getFieldsNotInSubschema.ts @@ -31,23 +31,34 @@ function collectSubFields(info: GraphQLResolveInfo, typeName: string): Record = info.schema.extensions?.['stitchingInfo']; - const selectionSetsByField = stitchingInfo?.selectionSetsByField; + const fieldNodesByField = stitchingInfo?.fieldNodesByField; - for (const responseName in subFieldNodes) { - const fieldName = subFieldNodes[responseName][0].name.value; - const fieldSelectionSet = selectionSetsByField?.[typeName]?.[fieldName]; - if (fieldSelectionSet != null) { - subFieldNodes = collectFields( - partialExecutionContext, - type, - fieldSelectionSet, - subFieldNodes, - visitedFragmentNames - ); + const subFieldNodesByFieldName = Object.create(null); + for (const responseKey in subFieldNodes) { + const fieldName = subFieldNodes[responseKey][0].name.value; + const additionalFieldNodes = fieldNodesByField?.[typeName]?.[fieldName]; + if (additionalFieldNodes) { + for (const additionalFieldNode of additionalFieldNodes) { + const additionalFieldName = additionalFieldNode.name.value; + if (subFieldNodesByFieldName[additionalFieldName] == null) { + subFieldNodesByFieldName[additionalFieldName] = [additionalFieldNode]; + } else { + subFieldNodesByFieldName[additionalFieldName].push(additionalFieldNode); + } + } + } + } + + for (const responseKey in subFieldNodes) { + const fieldName = subFieldNodes[responseKey][0].name.value; + if (subFieldNodesByFieldName[fieldName] == null) { + subFieldNodesByFieldName[fieldName] = subFieldNodes[responseKey]; + } else { + subFieldNodesByFieldName[fieldName].concat(subFieldNodes[responseKey]); } } - return subFieldNodes; + return subFieldNodesByFieldName; } export const getFieldsNotInSubschema = memoizeInfoAnd2Objects(function ( @@ -65,10 +76,9 @@ export const getFieldsNotInSubschema = memoizeInfoAnd2Objects(function ( const subFieldNodes = collectSubFields(info, typeName); let fieldsNotInSchema: Array = []; - for (const responseName in subFieldNodes) { - const fieldName = subFieldNodes[responseName][0].name.value; + for (const fieldName in subFieldNodes) { if (!(fieldName in fields)) { - fieldsNotInSchema = fieldsNotInSchema.concat(subFieldNodes[responseName]); + fieldsNotInSchema = fieldsNotInSchema.concat(subFieldNodes[fieldName]); } } diff --git a/packages/delegate/src/transforms/AddSelectionSets.ts b/packages/delegate/src/transforms/AddSelectionSets.ts index 4a5951c59bc..76c3a47d712 100644 --- a/packages/delegate/src/transforms/AddSelectionSets.ts +++ b/packages/delegate/src/transforms/AddSelectionSets.ts @@ -1,9 +1,8 @@ -import { SelectionSetNode, TypeInfo, Kind, FieldNode, SelectionNode, print } from 'graphql'; +import { SelectionSetNode, TypeInfo, Kind, FieldNode, SelectionNode } from 'graphql'; import { Maybe, ExecutionRequest } from '@graphql-tools/utils'; import { Transform, DelegationContext } from '../types'; -import { memoize2 } from '../memoize'; import VisitSelectionSets from './VisitSelectionSets'; @@ -11,12 +10,12 @@ export default class AddSelectionSets implements Transform { private readonly transformer: VisitSelectionSets; constructor( - selectionSetsByType: Record, - selectionSetsByField: Record>, + fieldNodesByType: Record>, + fieldNodesByField: Record>>, dynamicSelectionSetsByField: Record SelectionSetNode>>> ) { this.transformer = new VisitSelectionSets((node, typeInfo) => - visitSelectionSet(node, typeInfo, selectionSetsByType, selectionSetsByField, dynamicSelectionSetsByField) + visitSelectionSet(node, typeInfo, fieldNodesByType, fieldNodesByField, dynamicSelectionSetsByField) ); } @@ -32,30 +31,30 @@ export default class AddSelectionSets implements Transform { function visitSelectionSet( node: SelectionSetNode, typeInfo: TypeInfo, - selectionSetsByType: Record, - selectionSetsByField: Record>, + fieldNodesByType: Record>, + fieldNodesByField: Record>>, dynamicSelectionSetsByField: Record SelectionSetNode>>> ): Maybe { const parentType = typeInfo.getParentType(); - const newSelections: Map = new Map(); + const newSelections: Set = new Set(); if (parentType != null) { const parentTypeName = parentType.name; - addSelectionsToMap(newSelections, node); + addSelectionsToSet(newSelections, node.selections); - if (parentTypeName in selectionSetsByType) { - const selectionSet = selectionSetsByType[parentTypeName]; - addSelectionsToMap(newSelections, selectionSet); + const fieldNodes = fieldNodesByType[parentTypeName]; + if (fieldNodes) { + addSelectionsToSet(newSelections, fieldNodes); } - if (parentTypeName in selectionSetsByField) { + if (parentTypeName in fieldNodesByField) { for (const selection of node.selections) { if (selection.kind === Kind.FIELD) { - const name = selection.name.value; - const selectionSet = selectionSetsByField[parentTypeName][name]; - if (selectionSet != null) { - addSelectionsToMap(newSelections, selectionSet); + const fieldName = selection.name.value; + const fieldNodes = fieldNodesByField[parentTypeName][fieldName]; + if (fieldNodes != null) { + addSelectionsToSet(newSelections, fieldNodes); } } } @@ -70,7 +69,7 @@ function visitSelectionSet( for (const selectionSetFn of dynamicSelectionSets) { const selectionSet = selectionSetFn(selection); if (selectionSet != null) { - addSelectionsToMap(newSelections, selectionSet); + addSelectionsToSet(newSelections, selectionSet.selections); } } } @@ -85,8 +84,8 @@ function visitSelectionSet( } } -const addSelectionsToMap = memoize2(function (map: Map, selectionSet: SelectionSetNode): void { - for (const selection of selectionSet.selections) { - map.set(print(selection), selection); +function addSelectionsToSet(set: Set, selections: ReadonlyArray): void { + for (const selection of selections) { + set.add(selection); } -}); +} diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 5b0dcaee820..05933cfda1f 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -193,8 +193,8 @@ export type MergedTypeResolver> = ( export interface StitchingInfo> { subschemaMap: Map, Subschema>; - selectionSetsByType: Record; - selectionSetsByField: Record>; + fieldNodesByType: Record>; + fieldNodesByField: Record>>; dynamicSelectionSetsByField: Record SelectionSetNode>>>; mergedTypes: Record>; } diff --git a/packages/stitch/src/stitchingInfo.ts b/packages/stitch/src/stitchingInfo.ts index 062e25be43e..8446cbb0a85 100644 --- a/packages/stitch/src/stitchingInfo.ts +++ b/packages/stitch/src/stitchingInfo.ts @@ -4,22 +4,31 @@ import { Kind, SelectionSetNode, isObjectType, - isScalarType, getNamedType, GraphQLInterfaceType, - SelectionNode, print, isInterfaceType, isLeafType, + FieldNode, + isInputObjectType, + isUnionType, } from 'graphql'; -import { parseSelectionSet, TypeMap, IResolvers, IFieldResolverOptions, isSome } from '@graphql-tools/utils'; +import { + parseSelectionSet, + TypeMap, + IResolvers, + IFieldResolverOptions, + isSome, + GraphQLExecutionContext, +} from '@graphql-tools/utils'; import { MergedTypeResolver, Subschema, SubschemaConfig, MergedTypeInfo, StitchingInfo } from '@graphql-tools/delegate'; import { MergeTypeCandidate, MergeTypeFilter } from './types'; import { createMergedTypeResolver } from './createMergedTypeResolver'; +import { collectFields } from 'graphql/execution/execute'; export function createStitchingInfo>( subschemaMap: Map, Subschema>, @@ -27,57 +36,10 @@ export function createStitchingInfo>( mergeTypes?: boolean | Array | MergeTypeFilter ): StitchingInfo { const mergedTypes = createMergedTypes(typeCandidates, mergeTypes); - const selectionSetsByField: Record> = Object.create(null); - - for (const typeName in mergedTypes) { - const mergedTypeInfo = mergedTypes[typeName]; - if (mergedTypeInfo.selectionSets == null && mergedTypeInfo.fieldSelectionSets == null) { - continue; - } - - selectionSetsByField[typeName] = Object.create(null); - - for (const [subschemaConfig, selectionSet] of mergedTypeInfo.selectionSets) { - const schema = subschemaConfig.transformedSchema; - const type = schema.getType(typeName) as GraphQLObjectType; - const fields = type.getFields(); - for (const fieldName in fields) { - const field = fields[fieldName]; - const fieldType = getNamedType(field.type); - if (selectionSet && isLeafType(fieldType) && selectionSetContainsTopLevelField(selectionSet, fieldName)) { - continue; - } - if (selectionSetsByField[typeName][fieldName] == null) { - selectionSetsByField[typeName][fieldName] = { - kind: Kind.SELECTION_SET, - selections: [parseSelectionSet('{ __typename }', { noLocation: true }).selections[0]], - }; - } - selectionSetsByField[typeName][fieldName].selections = selectionSetsByField[typeName][ - fieldName - ].selections.concat(selectionSet.selections); - } - } - - for (const [, selectionSetFieldMap] of mergedTypeInfo.fieldSelectionSets) { - for (const fieldName in selectionSetFieldMap) { - if (selectionSetsByField[typeName][fieldName] == null) { - selectionSetsByField[typeName][fieldName] = { - kind: Kind.SELECTION_SET, - selections: [parseSelectionSet('{ __typename }', { noLocation: true }).selections[0]], - }; - } - selectionSetsByField[typeName][fieldName].selections = selectionSetsByField[typeName][ - fieldName - ].selections.concat(selectionSetFieldMap[fieldName].selections); - } - } - } - return { subschemaMap, - selectionSetsByType: Object.create(null), - selectionSetsByField, + fieldNodesByType: Object.create(null), + fieldNodesByField: Object.create(null), dynamicSelectionSetsByField: Object.create(null), mergedTypes, }; @@ -227,70 +189,147 @@ export function completeStitchingInfo>( resolvers: IResolvers, schema: GraphQLSchema ): StitchingInfo { - const { selectionSetsByType, selectionSetsByField, dynamicSelectionSetsByField } = stitchingInfo; + const { fieldNodesByType, fieldNodesByField, dynamicSelectionSetsByField, mergedTypes } = stitchingInfo; // must add __typename to query and mutation root types to handle type merging with nested root types // cannot add __typename to subscription root types, but they cannot be nested const rootTypes = [schema.getQueryType(), schema.getMutationType()]; for (const rootType of rootTypes) { if (rootType) { - selectionSetsByType[rootType.name] = parseSelectionSet('{ __typename }', { noLocation: true }); + fieldNodesByType[rootType.name] = [ + parseSelectionSet('{ __typename }', { noLocation: true }).selections[0] as FieldNode, + ]; } } - for (const typeName in resolvers) { - const type = resolvers[typeName]; - if (isScalarType(type)) { + const selectionSetsByField: Record>> = Object.create(null); + for (const typeName in mergedTypes) { + const mergedTypeInfo = mergedTypes[typeName]; + if (mergedTypeInfo.selectionSets == null && mergedTypeInfo.fieldSelectionSets == null) { continue; } - for (const fieldName in type) { - const field = type[fieldName] as IFieldResolverOptions; - if (field.selectionSet) { - if (typeof field.selectionSet === 'function') { - if (!(typeName in dynamicSelectionSetsByField)) { - dynamicSelectionSetsByField[typeName] = Object.create(null); - } - if (!(fieldName in dynamicSelectionSetsByField[typeName])) { - dynamicSelectionSetsByField[typeName][fieldName] = []; - } + for (const [subschemaConfig, selectionSet] of mergedTypeInfo.selectionSets) { + const schema = subschemaConfig.transformedSchema; + const type = schema.getType(typeName) as GraphQLObjectType; + const fields = type.getFields(); + for (const fieldName in fields) { + const field = fields[fieldName]; + const fieldType = getNamedType(field.type); + if (selectionSet && isLeafType(fieldType) && selectionSetContainsTopLevelField(selectionSet, fieldName)) { + continue; + } + updateSelectionSetMap(selectionSetsByField, typeName, fieldName, selectionSet, true); + } + } - dynamicSelectionSetsByField[typeName][fieldName].push(field.selectionSet); - } else { - const selectionSet = parseSelectionSet(field.selectionSet, { noLocation: true }); - if (!(typeName in selectionSetsByField)) { - selectionSetsByField[typeName] = Object.create(null); - } + for (const [, selectionSetFieldMap] of mergedTypeInfo.fieldSelectionSets) { + for (const fieldName in selectionSetFieldMap) { + const selectionSet = selectionSetFieldMap[fieldName]; + updateSelectionSetMap(selectionSetsByField, typeName, fieldName, selectionSet, true); + } + } + } - if (!(fieldName in selectionSetsByField[typeName])) { - selectionSetsByField[typeName][fieldName] = { - kind: Kind.SELECTION_SET, - selections: [], - }; - } - selectionSetsByField[typeName][fieldName].selections = selectionSetsByField[typeName][ - fieldName - ].selections.concat(selectionSet.selections); + for (const typeName in resolvers) { + const type = schema.getType(typeName); + if (type === undefined || isLeafType(type) || isInputObjectType(type) || isUnionType(type)) { + continue; + } + const resolver = resolvers[typeName]; + for (const fieldName in resolver) { + const field = resolver[fieldName] as IFieldResolverOptions; + if (typeof field.selectionSet === 'function') { + if (!(typeName in dynamicSelectionSetsByField)) { + dynamicSelectionSetsByField[typeName] = Object.create(null); } + + if (!(fieldName in dynamicSelectionSetsByField[typeName])) { + dynamicSelectionSetsByField[typeName][fieldName] = []; + } + + dynamicSelectionSetsByField[typeName][fieldName].push(field.selectionSet); + } else if (field.selectionSet) { + const selectionSet = parseSelectionSet(field.selectionSet, { noLocation: true }); + updateSelectionSetMap(selectionSetsByField, typeName, fieldName, selectionSet); } } } + const partialExecutionContext = { + schema, + variableValues: Object.create(null), + fragments: Object.create(null), + }; + + const fieldNodeMap = Object.create(null); + for (const typeName in selectionSetsByField) { - const typeSelectionSets = selectionSetsByField[typeName]; - for (const fieldName in typeSelectionSets) { - const consolidatedSelections: Map = new Map(); - const selectionSet = typeSelectionSets[fieldName]; - for (const selection of selectionSet.selections) { - consolidatedSelections.set(print(selection), selection); + const type = schema.getType(typeName) as GraphQLObjectType; + for (const fieldName in selectionSetsByField[typeName]) { + for (const selectionSet of selectionSetsByField[typeName][fieldName]) { + const fieldNodes = collectFields( + partialExecutionContext as GraphQLExecutionContext, + type, + selectionSet, + Object.create(null), + Object.create(null) + ); + + for (const responseKey in fieldNodes) { + for (const fieldNode of fieldNodes[responseKey]) { + const key = print(fieldNode); + if (fieldNodeMap[key] == null) { + fieldNodeMap[key] = fieldNode; + updateArrayMap(fieldNodesByField, typeName, fieldName, fieldNode); + } else { + updateArrayMap(fieldNodesByField, typeName, fieldName, fieldNodeMap[key]); + } + } + } } - selectionSet.selections = Array.from(consolidatedSelections.values()); } } return stitchingInfo; } +function updateSelectionSetMap( + map: Record>>, + typeName: string, + fieldName: string, + selectionSet: SelectionSetNode, + includeTypename?: boolean +): void { + if (includeTypename) { + const typenameSelectionSet = parseSelectionSet('{ __typename }', { noLocation: true }); + updateArrayMap(map, typeName, fieldName, selectionSet, typenameSelectionSet); + return; + } + + updateArrayMap(map, typeName, fieldName, selectionSet); +} + +function updateArrayMap( + map: Record>>, + typeName: string, + fieldName: string, + value: T, + initialValue?: T +): void { + if (map[typeName] == null) { + const initialItems = initialValue === undefined ? [value] : [initialValue, value]; + map[typeName] = { + [fieldName]: initialItems, + }; + } else if (map[typeName][fieldName] == null) { + const initialItems = initialValue === undefined ? [value] : [initialValue, value]; + map[typeName][fieldName] = initialItems; + } else { + map[typeName][fieldName].push(value); + } +} + export function addStitchingInfo>( stitchedSchema: GraphQLSchema, stitchingInfo: StitchingInfo