From c788f5421ad40767068c8fa5efdbd5d3498ca4b6 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Sat, 4 May 2024 00:42:56 +0300 Subject: [PATCH] feat(delegate): deduplicate upstream documents Changeset Fix tests Deduplicate more Test recursive spreads Fix tests Nested Refactor Use the new algo in more places --- .changeset/blue-camels-camp.md | 5 + packages/batch-execute/src/mergeRequests.ts | 17 +- .../batch-execute/tests/batchExecute.test.ts | 3 +- packages/delegate/src/createRequest.ts | 13 +- .../delegate/src/finalizeGatewayRequest.ts | 15 +- .../delegate/src/prepareGatewayDocument.ts | 40 ++-- .../stitch/src/createDelegationPlanBuilder.ts | 61 +++--- .../stitch/src/getFieldsNotInSubschema.ts | 46 ++--- packages/utils/src/SelectionSetBuilder.ts | 101 +++++++++ packages/utils/src/index.ts | 1 + .../utils/tests/SelectionSetBuilder.test.ts | 191 ++++++++++++++++++ .../transforms/TransformCompositeFields.ts | 21 +- 12 files changed, 406 insertions(+), 108 deletions(-) create mode 100644 .changeset/blue-camels-camp.md create mode 100644 packages/utils/src/SelectionSetBuilder.ts create mode 100644 packages/utils/tests/SelectionSetBuilder.test.ts diff --git a/.changeset/blue-camels-camp.md b/.changeset/blue-camels-camp.md new file mode 100644 index 00000000000..3f8fab7e212 --- /dev/null +++ b/.changeset/blue-camels-camp.md @@ -0,0 +1,5 @@ +--- +"@graphql-tools/delegate": patch +--- + +Deduplicate fields, inline fragment spreads and fragment spreads before sending the operation document to the subschema diff --git a/packages/batch-execute/src/mergeRequests.ts b/packages/batch-execute/src/mergeRequests.ts index e3e22ab492b..dff4af138a8 100644 --- a/packages/batch-execute/src/mergeRequests.ts +++ b/packages/batch-execute/src/mergeRequests.ts @@ -14,7 +14,11 @@ import { VariableNode, visit, } from 'graphql'; -import { ExecutionRequest, getOperationASTFromRequest } from '@graphql-tools/utils'; +import { + ExecutionRequest, + getOperationASTFromRequest, + SelectionSetBuilder, +} from '@graphql-tools/utils'; import { createPrefix } from './prefix.js'; /** @@ -60,7 +64,7 @@ export function mergeRequests( ): ExecutionRequest { const mergedVariables: Record = Object.create(null); const mergedVariableDefinitions: Array = []; - const mergedSelections: Array = []; + const mergedSelections = new SelectionSetBuilder(); const mergedFragmentDefinitions: Array = []; let mergedExtensions: Record = Object.create(null); @@ -70,7 +74,9 @@ export function mergeRequests( for (const def of prefixedRequests.document.definitions) { if (isOperationDefinition(def)) { - mergedSelections.push(...def.selectionSet.selections); + for (const selection of def.selectionSet.selections) { + mergedSelections.addSelection(selection); + } if (def.variableDefinitions) { mergedVariableDefinitions.push(...def.variableDefinitions); } @@ -90,10 +96,7 @@ export function mergeRequests( kind: Kind.OPERATION_DEFINITION, operation: operationType, variableDefinitions: mergedVariableDefinitions, - selectionSet: { - kind: Kind.SELECTION_SET, - selections: mergedSelections, - }, + selectionSet: mergedSelections.getSelectionSet(), }; const operationName = firstRequest.operationName ?? firstRequest.info?.operation?.name?.value; if (operationName) { diff --git a/packages/batch-execute/tests/batchExecute.test.ts b/packages/batch-execute/tests/batchExecute.test.ts index 7205fccfe21..13d76b2cd92 100644 --- a/packages/batch-execute/tests/batchExecute.test.ts +++ b/packages/batch-execute/tests/batchExecute.test.ts @@ -126,8 +126,7 @@ describe('batch execution', () => { ])) as ExecutionResult[]; const squishedDoc = executorDocument?.replace(/\s+/g, ' '); - expect(squishedDoc).toMatch('... on Query { _0_field1: field1 }'); - expect(squishedDoc).toMatch('... on Query { _1_field2: field2 }'); + expect(squishedDoc).toMatch('... on Query { _0_field1: field1 _1_field2: field2 }'); expect(first?.data).toEqual({ field1: '1' }); expect(second?.data).toEqual({ field2: '2' }); expect(executorCalls).toEqual(1); diff --git a/packages/delegate/src/createRequest.ts b/packages/delegate/src/createRequest.ts index 29bd15221aa..4d71178ac70 100644 --- a/packages/delegate/src/createRequest.ts +++ b/packages/delegate/src/createRequest.ts @@ -11,7 +11,6 @@ import { NameNode, OperationDefinitionNode, OperationTypeNode, - SelectionNode, SelectionSetNode, typeFromAST, VariableDefinitionNode, @@ -19,6 +18,7 @@ import { import { createVariableNameGenerator, ExecutionRequest, + SelectionSetBuilder, serializeInputValue, updateArgument, } from '@graphql-tools/utils'; @@ -59,21 +59,16 @@ export function createRequest({ if (selectionSet != null) { newSelectionSet = selectionSet; } else { - const selections: Array = []; + const selections = new SelectionSetBuilder(); for (const fieldNode of fieldNodes || []) { if (fieldNode.selectionSet) { for (const selection of fieldNode.selectionSet.selections) { - selections.push(selection); + selections.addSelection(selection); } } } - newSelectionSet = selections.length - ? { - kind: Kind.SELECTION_SET, - selections, - } - : undefined; + newSelectionSet = selections.getSize() ? selections.getSelectionSet() : undefined; const args = fieldNodes?.[0]?.arguments; if (args) { diff --git a/packages/delegate/src/finalizeGatewayRequest.ts b/packages/delegate/src/finalizeGatewayRequest.ts index 68fdcd03f54..8961622f92c 100644 --- a/packages/delegate/src/finalizeGatewayRequest.ts +++ b/packages/delegate/src/finalizeGatewayRequest.ts @@ -28,6 +28,7 @@ import { getDefinedRootType, implementsAbstractType, inspect, + SelectionSetBuilder, serializeInputValue, updateArgument, } from '@graphql-tools/utils'; @@ -179,7 +180,7 @@ function addVariablesToRootFields( const type = getDefinedRootType(targetSchema, operation.operation); - const newSelections: Array = []; + const selectionSetBuilder = new SelectionSetBuilder(); for (const selection of operation.selectionSet.selections) { if (selection.kind === Kind.FIELD) { @@ -198,25 +199,19 @@ function addVariablesToRootFields( if (targetField != null) { updateArguments(targetField, argumentNodeMap, variableDefinitionMap, newVariables, args); } - - newSelections.push({ + selectionSetBuilder.addSelection({ ...selection, arguments: Object.values(argumentNodeMap), }); } else { - newSelections.push(selection); + selectionSetBuilder.addSelection(selection); } } - const newSelectionSet: SelectionSetNode = { - kind: Kind.SELECTION_SET, - selections: newSelections, - }; - return { ...operation, variableDefinitions: Object.values(variableDefinitionMap), - selectionSet: newSelectionSet, + selectionSet: selectionSetBuilder.getSelectionSet(), }; }); diff --git a/packages/delegate/src/prepareGatewayDocument.ts b/packages/delegate/src/prepareGatewayDocument.ts index 0264c9f7495..304d6ff2665 100644 --- a/packages/delegate/src/prepareGatewayDocument.ts +++ b/packages/delegate/src/prepareGatewayDocument.ts @@ -11,7 +11,6 @@ import { isInterfaceType, isLeafType, Kind, - SelectionNode, SelectionSetNode, TypeInfo, visit, @@ -22,6 +21,7 @@ import { getRootTypeNames, implementsAbstractType, memoize2, + SelectionSetBuilder, } from '@graphql-tools/utils'; import { getDocumentMetadata } from './getDocumentMetadata.js'; import { StitchingInfo } from './types.js'; @@ -116,7 +116,7 @@ function visitSelectionSet( Record SelectionSetNode>> >, ): SelectionSetNode { - const newSelections = new Set(); + const selectionSetBuilder = new SelectionSetBuilder(); const maybeType = typeInfo.getParentType(); if (maybeType != null) { @@ -126,12 +126,12 @@ function visitSelectionSet( const fieldNodes = fieldNodesByType[parentTypeName]; if (fieldNodes) { for (const fieldNode of fieldNodes) { - newSelections.add(fieldNode); + selectionSetBuilder.addSelection(fieldNode); } } const interfaceExtensions = interfaceExtensionsMap[parentType.name]; - const interfaceExtensionFields: Array = []; + const interfaceExtensionFieldsBuilder = new SelectionSetBuilder(); for (const selection of node.selections) { if (selection.kind === Kind.INLINE_FRAGMENT) { @@ -139,7 +139,7 @@ function visitSelectionSet( const possibleTypes = possibleTypesMap[selection.typeCondition.name.value]; if (possibleTypes == null) { - newSelections.add(selection); + selectionSetBuilder.addSelection(selection); continue; } @@ -149,7 +149,9 @@ function visitSelectionSet( maybePossibleType != null && implementsAbstractType(schema, parentType, maybePossibleType) ) { - newSelections.add(generateInlineFragment(possibleTypeName, selection.selectionSet)); + selectionSetBuilder.addSelection( + generateInlineFragment(possibleTypeName, selection.selectionSet), + ); } } } @@ -157,7 +159,7 @@ function visitSelectionSet( const fragmentName = selection.name.value; if (!fragmentReplacements[fragmentName]) { - newSelections.add(selection); + selectionSetBuilder.addSelection(selection); continue; } @@ -169,7 +171,7 @@ function visitSelectionSet( maybeReplacementType != null && implementsAbstractType(schema, parentType, maybeType) ) { - newSelections.add({ + selectionSetBuilder.addSelection({ kind: Kind.FRAGMENT_SPREAD, name: { kind: Kind.NAME, @@ -184,7 +186,7 @@ function visitSelectionSet( const fieldNodes = fieldNodesByField[parentTypeName]?.[fieldName]; if (fieldNodes != null) { for (const fieldNode of fieldNodes) { - newSelections.add(fieldNode); + selectionSetBuilder.addSelection(fieldNode); } } @@ -194,22 +196,22 @@ function visitSelectionSet( const selectionSet = selectionSetFn(selection); if (selectionSet != null) { for (const selection of selectionSet.selections) { - newSelections.add(selection); + selectionSetBuilder.addSelection(selection); } } } } if (interfaceExtensions?.[fieldName]) { - interfaceExtensionFields.push(selection); + interfaceExtensionFieldsBuilder.addSelection(selection); } else { - newSelections.add(selection); + selectionSetBuilder.addSelection(selection); } } } if (reversePossibleTypesMap[parentType.name]) { - newSelections.add({ + selectionSetBuilder.addSelection({ kind: Kind.FIELD, name: { kind: Kind.NAME, @@ -218,15 +220,13 @@ function visitSelectionSet( }); } - if (interfaceExtensionFields.length) { + if (interfaceExtensionFieldsBuilder.getSize()) { const possibleTypes = possibleTypesMap[parentType.name]; if (possibleTypes != null) { for (const possibleType of possibleTypes) { - newSelections.add( - generateInlineFragment(possibleType, { - kind: Kind.SELECTION_SET, - selections: interfaceExtensionFields, - }), + const interfaceExtensionFields = interfaceExtensionFieldsBuilder.getSelectionSet(); + selectionSetBuilder.addSelection( + generateInlineFragment(possibleType, interfaceExtensionFields), ); } } @@ -234,7 +234,7 @@ function visitSelectionSet( return { ...node, - selections: Array.from(newSelections), + ...selectionSetBuilder.getSelectionSet(), }; } diff --git a/packages/stitch/src/createDelegationPlanBuilder.ts b/packages/stitch/src/createDelegationPlanBuilder.ts index 2c79a8ba8c1..ea69f13ae47 100644 --- a/packages/stitch/src/createDelegationPlanBuilder.ts +++ b/packages/stitch/src/createDelegationPlanBuilder.ts @@ -14,7 +14,7 @@ import { StitchingInfo, Subschema, } from '@graphql-tools/delegate'; -import { memoize1, memoize2, memoize3, memoize5 } from '@graphql-tools/utils'; +import { memoize1, memoize2, memoize3, memoize5, SelectionSetBuilder } from '@graphql-tools/utils'; import { extractUnavailableFields, getFieldsNotInSubschema } from './getFieldsNotInSubschema.js'; function calculateDelegationStage( @@ -62,11 +62,11 @@ function calculateDelegationStage( } } - const unproxiableFieldNodes: Array = []; + const unproxiableFieldNodes = new SelectionSetBuilder(); // 2. for each selection: - const delegationMap: Map = new Map(); + const delegationMap: Map = new Map(); for (const fieldNode of fieldNodes) { const fieldName = fieldNode.name.value; if (fieldName === '__typename') { @@ -85,7 +85,7 @@ function calculateDelegationStage( ), ); if (sourcesWithUnsatisfiedDependencies.length === sourceSubschemas.length) { - unproxiableFieldNodes.push(fieldNode); + unproxiableFieldNodes.addSelection(fieldNode); for (const source of sourcesWithUnsatisfiedDependencies) { if (!nonProxiableSubschemas.includes(source)) { nonProxiableSubschemas.push(source); @@ -99,20 +99,16 @@ function calculateDelegationStage( const uniqueSubschema: Subschema = uniqueFields[fieldNode.name.value]; if (uniqueSubschema != null) { if (!proxiableSubschemas.includes(uniqueSubschema)) { - unproxiableFieldNodes.push(fieldNode); + unproxiableFieldNodes.addSelection(fieldNode); continue; } - const existingSubschema = delegationMap.get(uniqueSubschema)?.selections as SelectionNode[]; - if (existingSubschema != null) { - existingSubschema.push(fieldNode); - } else { - delegationMap.set(uniqueSubschema, { - kind: Kind.SELECTION_SET, - selections: [fieldNode], - }); + let existingSelectionSetBuilder = delegationMap.get(uniqueSubschema); + if (existingSelectionSetBuilder == null) { + existingSelectionSetBuilder = new SelectionSetBuilder(); + delegationMap.set(uniqueSubschema, existingSelectionSetBuilder); } - + existingSelectionSetBuilder.addSelection(fieldNode); continue; } @@ -121,20 +117,20 @@ function calculateDelegationStage( let nonUniqueSubschemas: Array = nonUniqueFields[fieldNode.name.value]; if (nonUniqueSubschemas == null) { - unproxiableFieldNodes.push(fieldNode); + unproxiableFieldNodes.addSelection(fieldNode); continue; } nonUniqueSubschemas = nonUniqueSubschemas.filter(s => proxiableSubschemas.includes(s)); if (!nonUniqueSubschemas.length) { - unproxiableFieldNodes.push(fieldNode); + unproxiableFieldNodes.addSelection(fieldNode); continue; } const existingSubschema = nonUniqueSubschemas.find(s => delegationMap.has(s)); if (existingSubschema != null) { - // It is okay we previously explicitly check whether the map has the element. - (delegationMap.get(existingSubschema)!.selections as SelectionNode[]).push(fieldNode); + const existingSelectionSetBuilder = delegationMap.get(existingSubschema)!; + existingSelectionSetBuilder.addSelection(fieldNode); } else { let bestUniqueSubschema: Subschema = nonUniqueSubschemas[0]; let bestScore = Infinity; @@ -151,10 +147,12 @@ function calculateDelegationStage( fieldNode, fieldType => { if (!nonUniqueSubschema.merge?.[fieldType.name]) { - delegationMap.set(nonUniqueSubschema, { - kind: Kind.SELECTION_SET, - selections: [fieldNode], - }); + let existingSelectionSetBuilder = delegationMap.get(nonUniqueSubschema); + if (existingSelectionSetBuilder == null) { + existingSelectionSetBuilder = new SelectionSetBuilder(); + delegationMap.set(nonUniqueSubschema, existingSelectionSetBuilder); + } + existingSelectionSetBuilder.addSelection(fieldNode); // Ignore unresolvable fields return false; } @@ -168,18 +166,25 @@ function calculateDelegationStage( } } } - delegationMap.set(bestUniqueSubschema, { - kind: Kind.SELECTION_SET, - selections: [fieldNode], - }); + let existingSelectionSetBuilder = delegationMap.get(bestUniqueSubschema); + if (existingSelectionSetBuilder == null) { + existingSelectionSetBuilder = new SelectionSetBuilder(); + delegationMap.set(bestUniqueSubschema, existingSelectionSetBuilder); + } + existingSelectionSetBuilder.addSelection(fieldNode); } } return { - delegationMap, + delegationMap: new Map( + [...delegationMap].map(([subschema, selectionSetBuilder]) => [ + subschema, + selectionSetBuilder.getSelectionSet(), + ]), + ), proxiableSubschemas, nonProxiableSubschemas, - unproxiableFieldNodes, + unproxiableFieldNodes: unproxiableFieldNodes.getSelectionSet().selections as Array, }; } diff --git a/packages/stitch/src/getFieldsNotInSubschema.ts b/packages/stitch/src/getFieldsNotInSubschema.ts index f8c66a620c9..047bbfd5953 100644 --- a/packages/stitch/src/getFieldsNotInSubschema.ts +++ b/packages/stitch/src/getFieldsNotInSubschema.ts @@ -14,11 +14,10 @@ import { isObjectType, isUnionType, Kind, - SelectionNode, SelectionSetNode, } from 'graphql'; import { StitchingInfo } from '@graphql-tools/delegate'; -import { collectSubFields, Maybe } from '@graphql-tools/utils'; +import { collectSubFields, Maybe, SelectionSetBuilder } from '@graphql-tools/utils'; export function getFieldsNotInSubschema( schema: GraphQLSchema, @@ -42,12 +41,12 @@ export function getFieldsNotInSubschema( const fields = subschemaType.getFields(); - const fieldsNotInSchema = new Set(); + const fieldsNotInSchema = new SelectionSetBuilder(); for (const [, subFieldNodes] of subFieldNodesByResponseKey) { const fieldName = subFieldNodes[0].name.value; if (!fields[fieldName]) { for (const subFieldNode of subFieldNodes) { - fieldsNotInSchema.add(subFieldNode); + fieldsNotInSchema.addSelection(subFieldNode); } } else { const field = fields[fieldName]; @@ -59,7 +58,7 @@ export function getFieldsNotInSubschema( (fieldType, selection) => !fieldNodesByField?.[fieldType.name]?.[selection.name.value], ); if (unavailableFields.length) { - fieldsNotInSchema.add({ + fieldsNotInSchema.addSelection({ ...subFieldNode, selectionSet: { kind: Kind.SELECTION_SET, @@ -75,15 +74,15 @@ export function getFieldsNotInSubschema( if (fieldNode.name.value !== '__typename' && !fields[fieldNode.name.value]) { // consider node that depends on something not in the schema as not in the schema for (const subFieldNode of subFieldNodes) { - fieldsNotInSchema.add(subFieldNode); + fieldsNotInSchema.addSelection(subFieldNode); } - fieldsNotInSchema.add(fieldNode); + fieldsNotInSchema.addSelection(fieldNode); } } } } - return Array.from(fieldsNotInSchema); + return fieldsNotInSchema.getSelectionSet().selections as Array; } export function extractUnavailableFieldsFromSelectionSet( @@ -96,7 +95,7 @@ export function extractUnavailableFieldsFromSelectionSet( return []; } if (isUnionType(fieldType)) { - const unavailableSelections: SelectionNode[] = []; + const unavailableSelections = new SelectionSetBuilder(); for (const type of fieldType.getTypes()) { // Exclude other inline fragments const fieldSelectionExcluded: SelectionSetNode = { @@ -109,19 +108,20 @@ export function extractUnavailableFieldsFromSelectionSet( : true, ), }; - unavailableSelections.push( - ...extractUnavailableFieldsFromSelectionSet( - schema, - type, - fieldSelectionExcluded, - shouldAdd, - ), + const fieldSelections = extractUnavailableFieldsFromSelectionSet( + schema, + type, + fieldSelectionExcluded, + shouldAdd, ); + for (const selection of fieldSelections) { + unavailableSelections.addSelection(selection); + } } - return unavailableSelections; + return unavailableSelections.getSelectionSet().selections; } const subFields = fieldType.getFields(); - const unavailableSelections: SelectionNode[] = []; + const unavailableSelections = new SelectionSetBuilder(); for (const selection of fieldSelectionSet.selections) { if (selection.kind === Kind.FIELD) { if (selection.name.value === '__typename') { @@ -131,7 +131,7 @@ export function extractUnavailableFieldsFromSelectionSet( const selectionField = subFields[fieldName]; if (!selectionField) { if (shouldAdd(fieldType, selection)) { - unavailableSelections.push(selection); + unavailableSelections.addSelection(selection); } } else { const unavailableSubFields = extractUnavailableFields( @@ -141,7 +141,7 @@ export function extractUnavailableFieldsFromSelectionSet( shouldAdd, ); if (unavailableSubFields.length) { - unavailableSelections.push({ + unavailableSelections.addSelection({ ...selection, selectionSet: { kind: Kind.SELECTION_SET, @@ -166,7 +166,7 @@ export function extractUnavailableFieldsFromSelectionSet( shouldAdd, ); if (unavailableFields.length) { - unavailableSelections.push({ + unavailableSelections.addSelection({ ...selection, selectionSet: { kind: Kind.SELECTION_SET, @@ -175,11 +175,11 @@ export function extractUnavailableFieldsFromSelectionSet( }); } } else { - unavailableSelections.push(selection); + unavailableSelections.addSelection(selection); } } } - return unavailableSelections; + return unavailableSelections.getSelectionSet().selections; } export function extractUnavailableFields( diff --git a/packages/utils/src/SelectionSetBuilder.ts b/packages/utils/src/SelectionSetBuilder.ts new file mode 100644 index 00000000000..a317eb03dac --- /dev/null +++ b/packages/utils/src/SelectionSetBuilder.ts @@ -0,0 +1,101 @@ +import { FieldNode, Kind, SelectionNode, SelectionSetNode } from 'graphql'; + +export class SelectionSetBuilder { + fieldNodeMap = new Map(); + fieldSelections = new Map(); + fragmentSpreads = new Set(); + inlineFragments = new Map(); + constructor() {} + addSelection(selection: SelectionNode) { + switch (selection.kind) { + case Kind.FRAGMENT_SPREAD: { + this.fragmentSpreads.add(selection.name.value); + break; + } + case Kind.INLINE_FRAGMENT: { + if (!selection.typeCondition) { + for (const subSelection of selection.selectionSet.selections) { + this.addSelection(subSelection); + } + break; + } + let inlineFragmentBuilder = this.inlineFragments.get(selection.typeCondition.name.value); + if (!inlineFragmentBuilder) { + inlineFragmentBuilder = new SelectionSetBuilder(); + this.inlineFragments.set(selection.typeCondition.name.value, inlineFragmentBuilder); + } + for (const subSelection of selection.selectionSet.selections) { + if (subSelection.kind === Kind.FIELD && this.fieldNodeMap.has(subSelection.name.value)) { + if (subSelection.selectionSet) { + let fieldSelections = this.fieldSelections.get(subSelection.name.value); + if (!fieldSelections) { + fieldSelections = new SelectionSetBuilder(); + this.fieldSelections.set(subSelection.name.value, fieldSelections); + } + for (const subSubSelection of subSelection.selectionSet.selections) { + fieldSelections.addSelection(subSubSelection); + } + } + continue; + } + inlineFragmentBuilder.addSelection(subSelection); + } + break; + } + case Kind.FIELD: { + const responseKey = selection.alias?.value || selection.name.value; + this.fieldNodeMap.set(responseKey, selection); + let fieldSelections = this.fieldSelections.get(responseKey); + if (!fieldSelections) { + fieldSelections = new SelectionSetBuilder(); + this.fieldSelections.set(responseKey, fieldSelections); + } + if (selection.selectionSet) { + for (const subSelection of selection.selectionSet.selections) { + fieldSelections.addSelection(subSelection); + } + } + break; + } + } + } + + getSelectionSet(): SelectionSetNode { + const selections: SelectionNode[] = []; + for (const fieldNode of this.fieldNodeMap.values()) { + selections.push(fieldNode); + } + for (const fragmentSpread of this.fragmentSpreads) { + selections.push({ + kind: Kind.FRAGMENT_SPREAD, + name: { + kind: Kind.NAME, + value: fragmentSpread, + }, + }); + } + for (const [typeName, inlineFragmentBuilder] of this.inlineFragments) { + if (inlineFragmentBuilder.getSize() > 0) { + selections.push({ + kind: Kind.INLINE_FRAGMENT, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: typeName, + }, + }, + selectionSet: inlineFragmentBuilder.getSelectionSet(), + }); + } + } + return { + kind: Kind.SELECTION_SET, + selections, + }; + } + + getSize() { + return this.fieldNodeMap.size + this.fragmentSpreads.size + this.inlineFragments.size; + } +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index c2bd2e5ab05..09c9b0273a5 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -54,3 +54,4 @@ export * from './jsutils.js'; export * from './directives.js'; export * from './mergeIncrementalResult.js'; export * from './debugTimer.js'; +export * from './SelectionSetBuilder.js'; diff --git a/packages/utils/tests/SelectionSetBuilder.test.ts b/packages/utils/tests/SelectionSetBuilder.test.ts new file mode 100644 index 00000000000..8d0faf15742 --- /dev/null +++ b/packages/utils/tests/SelectionSetBuilder.test.ts @@ -0,0 +1,191 @@ +import { Kind, print } from 'graphql'; +import { SelectionSetBuilder } from '../src/SelectionSetBuilder'; +import '../../testing/to-be-similar-gql-doc'; +import { parseSelectionSet } from '../src/selectionSets'; + +describe('SelectionSetBuilder', () => { + it('deduplicate fields', () => { + const selectionSetBuilder = new SelectionSetBuilder(); + selectionSetBuilder.addSelection({ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'a', + }, + }); + selectionSetBuilder.addSelection({ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'b', + }, + }); + selectionSetBuilder.addSelection({ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'a', + }, + }); + expect(selectionSetBuilder.getSelectionSet()).toMatchObject({ + selections: [ + { kind: 'Field', name: { value: 'a' } }, + { kind: 'Field', name: { value: 'b' } }, + ], + }); + }); + it('deduplicate fragment spreads', () => { + const selectionSetBuilder = new SelectionSetBuilder(); + selectionSetBuilder.addSelection({ + kind: Kind.FRAGMENT_SPREAD, + name: { + kind: Kind.NAME, + value: 'a', + }, + }); + selectionSetBuilder.addSelection({ + kind: Kind.FRAGMENT_SPREAD, + name: { + kind: Kind.NAME, + value: 'b', + }, + }); + selectionSetBuilder.addSelection({ + kind: Kind.FRAGMENT_SPREAD, + name: { + kind: Kind.NAME, + value: 'a', + }, + }); + expect(selectionSetBuilder.getSelectionSet()).toMatchObject({ + selections: [ + { kind: 'FragmentSpread', name: { value: 'a' } }, + { kind: 'FragmentSpread', name: { value: 'b' } }, + ], + }); + }); + it('deduplicate inline fragments', () => { + const selectionSetBuilder = new SelectionSetBuilder(); + selectionSetBuilder.addSelection({ + kind: Kind.INLINE_FRAGMENT, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'a', + }, + }, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'a', + }, + }, + ], + }, + }); + selectionSetBuilder.addSelection({ + kind: Kind.INLINE_FRAGMENT, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'a', + }, + }, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'b', + }, + }, + ], + }, + }); + selectionSetBuilder.addSelection({ + kind: Kind.INLINE_FRAGMENT, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'a', + }, + }, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'a', + }, + }, + ], + }, + }); + expect(selectionSetBuilder.getSelectionSet()).toMatchObject({ + selections: [ + { + kind: 'InlineFragment', + typeCondition: { kind: 'NamedType', name: { value: 'a' } }, + selectionSet: { + selections: [ + { kind: 'Field', name: { value: 'a' } }, + { kind: 'Field', name: { value: 'b' } }, + ], + }, + }, + ], + }); + }); + it('deduplicate inline fragments and fields', () => { + const selectionSetBuilder = new SelectionSetBuilder(); + const selectionSet = parseSelectionSet( + /* GraphQL */ ` + { + __typename + id + ... on User { + __typename + id + name + } + ... on Post { + __typename + id + title + } + ... { + extensions + } + } + `, + { noLocation: true }, + ); + for (const selection of selectionSet.selections) { + selectionSetBuilder.addSelection(selection); + } + expect(print(selectionSetBuilder.getSelectionSet())).toBeSimilarGqlDoc(/* GraphQL */ ` + { + __typename + id + extensions + ... on User { + name + } + ... on Post { + title + } + } + `); + }); +}); diff --git a/packages/wrap/src/transforms/TransformCompositeFields.ts b/packages/wrap/src/transforms/TransformCompositeFields.ts index 235eda99707..8a26aeb6373 100644 --- a/packages/wrap/src/transforms/TransformCompositeFields.ts +++ b/packages/wrap/src/transforms/TransformCompositeFields.ts @@ -17,6 +17,7 @@ import { MapperKind, mapSchema, Maybe, + SelectionSetBuilder, visitData, } from '@graphql-tools/utils'; import { @@ -149,12 +150,12 @@ export default class TransformCompositeFields> } const parentTypeName = parentType.name; - let newSelections: Array = []; + const newSelections = new SelectionSetBuilder(); let isTypenameSelected = false; for (const selection of node.selections) { if (selection.kind !== Kind.FIELD) { - newSelections.push(selection); + newSelections.addSelection(selection); continue; } @@ -187,26 +188,28 @@ export default class TransformCompositeFields> if (transformedSelection == null) { continue; } else if (Array.isArray(transformedSelection)) { - newSelections = newSelections.concat(transformedSelection); + for (const transformedSelectionNode of transformedSelection) { + newSelections.addSelection(transformedSelectionNode); + } continue; } else if (transformedSelection.kind !== Kind.FIELD) { - newSelections.push(transformedSelection); + newSelections.addSelection(transformedSelection); continue; } const typeMapping = this.mapping[parentTypeName]; if (typeMapping == null) { - newSelections.push(transformedSelection); + newSelections.addSelection(transformedSelection); continue; } const oldName = this.mapping[parentTypeName][newName]; if (oldName == null) { - newSelections.push(transformedSelection); + newSelections.addSelection(transformedSelection); continue; } - newSelections.push({ + newSelections.addSelection({ ...transformedSelection, name: { kind: Kind.NAME, @@ -225,7 +228,7 @@ export default class TransformCompositeFields> (this.dataTransformer != null || this.errorsTransformer != null) && (this.subscriptionTypeName == null || parentTypeName !== this.subscriptionTypeName) ) { - newSelections.push({ + newSelections.addSelection({ kind: Kind.FIELD, name: { kind: Kind.NAME, @@ -236,7 +239,7 @@ export default class TransformCompositeFields> return { ...node, - selections: newSelections, + ...newSelections.getSelectionSet(), }; } }