diff --git a/.changeset/kind-dolls-wonder.md b/.changeset/kind-dolls-wonder.md new file mode 100644 index 00000000000..2868420dc21 --- /dev/null +++ b/.changeset/kind-dolls-wonder.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/delegate': patch +--- + +Performance improvements on upstream request execution diff --git a/packages/delegate/src/OverlappingAliasesTransform.ts b/packages/delegate/src/OverlappingAliasesTransform.ts index b457f47aefe..8605f57b395 100644 --- a/packages/delegate/src/OverlappingAliasesTransform.ts +++ b/packages/delegate/src/OverlappingAliasesTransform.ts @@ -1,5 +1,5 @@ -import { isNullableType, Kind, visit } from 'graphql'; -import { ExecutionRequest, ExecutionResult } from '@graphql-tools/utils'; +import { ASTNode, isNullableType, Kind, visit } from 'graphql'; +import { ASTVisitorKeyMap, ExecutionRequest, ExecutionResult } from '@graphql-tools/utils'; import { DelegationContext, Transform } from './types.js'; const OverlappingAliases = Symbol('OverlappingAliases'); @@ -16,65 +16,80 @@ export class OverlappingAliasesTransform delegationContext: DelegationContext, transformationContext: OverlappingAliasesContext, ) { - const newDocument = visit(request.document, { - [Kind.SELECTION_SET]: node => { - const seenNonNullable = new Set(); - const seenNullable = new Set(); - return { - ...node, - selections: node.selections.map(selection => { - if (selection.kind === Kind.INLINE_FRAGMENT) { - const selectionTypeName = selection.typeCondition?.name.value; - if (selectionTypeName) { - const selectionType = - delegationContext.transformedSchema.getType(selectionTypeName); - if (selectionType && 'getFields' in selectionType) { - const selectionTypeFields = selectionType.getFields(); - return { - ...selection, - selectionSet: { - ...selection.selectionSet, - selections: selection.selectionSet.selections.map(subSelection => { - if (subSelection.kind === Kind.FIELD) { - const fieldName = subSelection.name.value; - if (!subSelection.alias) { - const field = selectionTypeFields[fieldName]; - if (field) { - let currentNullable: boolean; - if (isNullableType(field.type)) { - seenNullable.add(fieldName); - currentNullable = true; - } else { - seenNonNullable.add(fieldName); - currentNullable = false; - } - if (seenNullable.has(fieldName) && seenNonNullable.has(fieldName)) { - transformationContext[OverlappingAliases] = true; - return { - ...subSelection, - alias: { - kind: Kind.NAME, - value: currentNullable - ? `_nullable_${fieldName}` - : `_nonNullable_${fieldName}`, - }, - }; - } + const visitorKeys: ASTVisitorKeyMap = { + Document: ['definitions'], + OperationDefinition: ['selectionSet'], + SelectionSet: ['selections'], + Field: ['selectionSet'], + InlineFragment: ['selectionSet'], + FragmentDefinition: ['selectionSet'], + }; + const seenNonNullableMap = new WeakMap>(); + const seenNullableMap = new WeakMap>(); + const newDocument = visit( + request.document, + { + [Kind.INLINE_FRAGMENT](selection, _key, parent, _path, _ancestors) { + if (Array.isArray(parent)) { + const selectionTypeName = selection.typeCondition?.name.value; + if (selectionTypeName) { + const selectionType = delegationContext.transformedSchema.getType(selectionTypeName); + if (selectionType && 'getFields' in selectionType) { + const selectionTypeFields = selectionType.getFields(); + let seenNonNullable = seenNonNullableMap.get(parent); + if (!seenNonNullable) { + seenNonNullable = new Set(); + seenNonNullableMap.set(parent, seenNonNullable); + } + let seenNullable = seenNullableMap.get(parent); + if (!seenNullable) { + seenNullable = new Set(); + seenNullableMap.set(parent, seenNullable); + } + return { + ...selection, + selectionSet: { + ...selection.selectionSet, + selections: selection.selectionSet.selections.map(subSelection => { + if (subSelection.kind === Kind.FIELD) { + const fieldName = subSelection.name.value; + if (!subSelection.alias) { + const field = selectionTypeFields[fieldName]; + if (field) { + let currentNullable: boolean; + if (isNullableType(field.type)) { + seenNullable.add(fieldName); + currentNullable = true; + } else { + seenNonNullable.add(fieldName); + currentNullable = false; + } + if (seenNullable.has(fieldName) && seenNonNullable.has(fieldName)) { + transformationContext[OverlappingAliases] = true; + return { + ...subSelection, + alias: { + kind: Kind.NAME, + value: currentNullable + ? `_nullable_${fieldName}` + : `_nonNullable_${fieldName}`, + }, + }; } } } - return subSelection; - }), - }, - }; - } + } + return subSelection; + }), + }, + }; } } - return selection; - }), - }; + } + }, }, - }); + visitorKeys as any, + ); return { ...request, document: newDocument, diff --git a/packages/delegate/src/finalizeGatewayRequest.ts b/packages/delegate/src/finalizeGatewayRequest.ts index 1621bcfa309..bcfd48245be 100644 --- a/packages/delegate/src/finalizeGatewayRequest.ts +++ b/packages/delegate/src/finalizeGatewayRequest.ts @@ -174,44 +174,58 @@ export function finalizeGatewayRequest( let cleanedUpDocument = newDocument; // TODO: Optimize this internally later - cleanedUpDocument = visit(newDocument, { - // Cleanup extra __typename fields - SelectionSet: { - leave(node) { - const { hasTypeNameField, selections } = filterTypenameFields(node.selections); - if (hasTypeNameField) { - selections.unshift({ - kind: Kind.FIELD, - name: { - kind: Kind.NAME, - value: '__typename', - }, - }); - } - return { - ...node, - selections, - }; - }, - }, - // Cleanup empty inline fragments - InlineFragment: { - leave(node) { - // No need __typename in inline fragment - const { selections } = filterTypenameFields(node.selectionSet.selections); - if (selections.length === 0) { - return null; - } - return { - ...node, - selectionSet: { - ...node.selectionSet, + const visitorKeys: ASTVisitorKeyMap = { + Document: ['definitions'], + OperationDefinition: ['selectionSet'], + SelectionSet: ['selections'], + Field: ['selectionSet'], + InlineFragment: ['selectionSet'], + FragmentDefinition: ['selectionSet'], + }; + cleanedUpDocument = visit( + newDocument, + { + // Cleanup extra __typename fields + [Kind.SELECTION_SET]: { + leave(node) { + const { hasTypeNameField, selections } = filterTypenameFields(node.selections); + if (hasTypeNameField) { + selections.unshift({ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }); + } + return { + ...node, selections, - }, - }; + }; + }, + }, + // Cleanup empty inline fragments + [Kind.INLINE_FRAGMENT]: { + leave(node) { + // No need __typename in inline fragment + const { selections } = filterTypenameFields(node.selectionSet.selections); + if (selections.length === 0) { + return null; + } + return { + ...node, + selectionSet: { + ...node.selectionSet, + selections, + }, + // @defer is not available for the communication between the gw and subgraph + directives: node.directives?.filter?.(directive => directive.name.value !== 'defer'), + }; + }, }, }, - }); + visitorKeys as any, + ); return { ...originalRequest, diff --git a/packages/delegate/src/prepareGatewayDocument.ts b/packages/delegate/src/prepareGatewayDocument.ts index d322e2a055a..b0d4ce4d326 100644 --- a/packages/delegate/src/prepareGatewayDocument.ts +++ b/packages/delegate/src/prepareGatewayDocument.ts @@ -46,22 +46,31 @@ export function prepareGatewayDocument( } const visitedSelections = new WeakSet(); - wrappedConcreteTypesDocument = visit(wrappedConcreteTypesDocument, { - [Kind.SELECTION_SET](node) { - const newSelections: Array = []; - for (const selectionNode of node.selections) { - if ( - selectionNode.kind === Kind.INLINE_FRAGMENT && - selectionNode.typeCondition != null && - !visitedSelections.has(selectionNode) - ) { - visitedSelections.add(selectionNode); - const typeName = selectionNode.typeCondition.name.value; - const gatewayType = infoSchema.getType(typeName); - const subschemaType = transformedSchema.getType(typeName); - if (isAbstractType(gatewayType)) { - const possibleTypes = infoSchema.getPossibleTypes(gatewayType); - if (isAbstractType(subschemaType)) { + const visitorKeys: ASTVisitorKeyMap = { + Document: ['definitions'], + OperationDefinition: ['selectionSet'], + SelectionSet: ['selections'], + Field: ['selectionSet'], + InlineFragment: ['selectionSet'], + FragmentDefinition: ['selectionSet'], + }; + wrappedConcreteTypesDocument = visit( + wrappedConcreteTypesDocument, + { + [Kind.SELECTION_SET](node) { + const newSelections: Array = []; + for (const selectionNode of node.selections) { + if ( + selectionNode.kind === Kind.INLINE_FRAGMENT && + selectionNode.typeCondition != null && + !visitedSelections.has(selectionNode) + ) { + visitedSelections.add(selectionNode); + const typeName = selectionNode.typeCondition.name.value; + const gatewayType = infoSchema.getType(typeName); + const subschemaType = transformedSchema.getType(typeName); + if (isAbstractType(gatewayType) && isAbstractType(subschemaType)) { + const possibleTypes = infoSchema.getPossibleTypes(gatewayType); const possibleTypesInSubschema = transformedSchema.getPossibleTypes(subschemaType); const extraTypesForSubschema = new Set(); for (const possibleType of possibleTypes) { @@ -93,34 +102,35 @@ export function prepareGatewayDocument( }); } } - } - const typeInSubschema = transformedSchema.getType(typeName); - if (!typeInSubschema) { - for (const selection of selectionNode.selectionSet.selections) { - newSelections.push(selection); + const typeInSubschema = transformedSchema.getType(typeName); + if (!typeInSubschema) { + for (const selection of selectionNode.selectionSet.selections) { + newSelections.push(selection); + } } - } - if (typeInSubschema && 'getFields' in typeInSubschema) { - const fieldMap = typeInSubschema.getFields(); - for (const selection of selectionNode.selectionSet.selections) { - if (selection.kind === Kind.FIELD) { - const fieldName = selection.name.value; - const field = fieldMap[fieldName]; - if (!field) { - newSelections.push(selection); + if (typeInSubschema && 'getFields' in typeInSubschema) { + const fieldMap = typeInSubschema.getFields(); + for (const selection of selectionNode.selectionSet.selections) { + if (selection.kind === Kind.FIELD) { + const fieldName = selection.name.value; + const field = fieldMap[fieldName]; + if (!field) { + newSelections.push(selection); + } } } } } + newSelections.push(selectionNode); } - newSelections.push(selectionNode); - } - return { - ...node, - selections: newSelections, - }; + return { + ...node, + selections: newSelections, + }; + }, }, - }); + visitorKeys as any, + ); const { possibleTypesMap, diff --git a/packages/federation/src/supergraph.ts b/packages/federation/src/supergraph.ts index 7f5e2812c1f..c7f0cc656ff 100644 --- a/packages/federation/src/supergraph.ts +++ b/packages/federation/src/supergraph.ts @@ -39,6 +39,7 @@ import { MergedTypeConfig, SubschemaConfig, subtractSelectionSets, + Transform, } from '@graphql-tools/delegate'; import { buildHTTPExecutor, HTTPExecutorOptions } from '@graphql-tools/executor-http'; import { @@ -50,6 +51,7 @@ import { ValidationLevel, } from '@graphql-tools/stitch'; import { + ASTVisitorKeyMap, createGraphQLError, isPromise, memoize1, @@ -911,61 +913,66 @@ export function getStitchingOptionsFromSupergraphSdl( } const typeNameProvidedMap = subgraphTypeNameProvidedMap.get(subgraphName); const externalFieldMap = subgraphExternalFieldMap.get(subgraphName); + const transforms: Transform[] = []; + if (externalFieldMap?.size && extendedSubgraphTypes.some(isInterfaceType)) { + const typeInfo = new TypeInfo(schema); + const visitorKeys: ASTVisitorKeyMap = { + Document: ['definitions'], + OperationDefinition: ['selectionSet'], + SelectionSet: ['selections'], + Field: ['selectionSet'], + InlineFragment: ['selectionSet'], + FragmentDefinition: ['selectionSet'], + }; + transforms.push({ + transformRequest(request) { + return { + ...request, + document: visit( + request.document, + visitWithTypeInfo(typeInfo, { + // To avoid resolving unresolvable interface fields + [Kind.FIELD](node) { + if (node.name.value !== '__typename') { + const parentType = typeInfo.getParentType(); + if (isInterfaceType(parentType)) { + const providedInterfaceFields = typeNameProvidedMap?.get(parentType.name); + const implementations = schema.getPossibleTypes(parentType); + for (const implementation of implementations) { + const externalFields = externalFieldMap?.get(implementation.name); + const providedFields = typeNameProvidedMap?.get(implementation.name); + if ( + !providedInterfaceFields?.has(node.name.value) && + !providedFields?.has(node.name.value) && + externalFields?.has(node.name.value) + ) { + throw createGraphQLError( + `Was not able to find any options for ${node.name.value}: This shouldn't have happened.`, + { + extensions: { + CRITICAL_ERROR: true, + }, + }, + ); + } + } + } + } + }, + }), + visitorKeys as any, + ), + }; + }, + }); + } const subschemaConfig: FederationSubschemaConfig = { name: subgraphName, endpoint, schema, executor, merge: mergeConfig, - transforms: [ - { - transformRequest(request) { - const typeInfo = new TypeInfo(schema); - return { - ...request, - document: visit( - request.document, - visitWithTypeInfo(typeInfo, { - [Kind.DIRECTIVE](node) { - if (node.name.value === 'defer') { - // @defer is not available for the communication between the gw and subgraph - return null; - } - }, - // To avoid resolving unresolvable interface fields - [Kind.FIELD](node) { - if (node.name.value !== '__typename') { - const parentType = typeInfo.getParentType(); - if (isInterfaceType(parentType)) { - const providedInterfaceFields = typeNameProvidedMap?.get(parentType.name); - const implementations = schema.getPossibleTypes(parentType); - for (const implementation of implementations) { - const externalFields = externalFieldMap?.get(implementation.name); - const providedFields = typeNameProvidedMap?.get(implementation.name); - if ( - !providedInterfaceFields?.has(node.name.value) && - !providedFields?.has(node.name.value) && - externalFields?.has(node.name.value) - ) { - throw createGraphQLError( - `Was not able to find any options for ${node.name.value}: This shouldn't have happened.`, - { - extensions: { - CRITICAL_ERROR: true, - }, - }, - ); - } - } - } - } - }, - }), - ), - }; - }, - }, - ], + transforms, batch: opts.batch, batchingOptions: opts.batchingOptions, };