From 074fad4144095fbefe449ced397b7707963bd7aa Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 29 Apr 2024 11:22:24 -0400 Subject: [PATCH] Merge list fields correctly (#6109) * Merge list fields correctly * Changesets * Tests --- .changeset/brown-beers-relax.md | 5 + .changeset/red-balloons-cheer.md | 5 + .changeset/tall-icons-poke.md | 5 + packages/delegate/src/mergeFields.ts | 12 +- packages/federation/src/supergraph.ts | 8 +- packages/federation/test/.gitignore | 1 + .../stitch/src/getFieldsNotInSubschema.ts | 14 +++ .../tests/extractUnavailableFields.test.ts | 112 ++++++++++++++++++ 8 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 .changeset/brown-beers-relax.md create mode 100644 .changeset/red-balloons-cheer.md create mode 100644 .changeset/tall-icons-poke.md create mode 100644 packages/federation/test/.gitignore create mode 100644 packages/stitch/tests/extractUnavailableFields.test.ts diff --git a/.changeset/brown-beers-relax.md b/.changeset/brown-beers-relax.md new file mode 100644 index 00000000000..33da278e074 --- /dev/null +++ b/.changeset/brown-beers-relax.md @@ -0,0 +1,5 @@ +--- +"@graphql-tools/federation": patch +--- + +Show responses in debug logging with `DEBUG` env var diff --git a/.changeset/red-balloons-cheer.md b/.changeset/red-balloons-cheer.md new file mode 100644 index 00000000000..18c8683ab8e --- /dev/null +++ b/.changeset/red-balloons-cheer.md @@ -0,0 +1,5 @@ +--- +"@graphql-tools/delegate": patch +--- + +Merge list fields correctly diff --git a/.changeset/tall-icons-poke.md b/.changeset/tall-icons-poke.md new file mode 100644 index 00000000000..662bafe0709 --- /dev/null +++ b/.changeset/tall-icons-poke.md @@ -0,0 +1,5 @@ +--- +"@graphql-tools/stitch": patch +--- + +Exclude fields with `__typename` while extracting missing fields for the type merging diff --git a/packages/delegate/src/mergeFields.ts b/packages/delegate/src/mergeFields.ts index b1fd1542f58..7cacadcc89f 100644 --- a/packages/delegate/src/mergeFields.ts +++ b/packages/delegate/src/mergeFields.ts @@ -145,7 +145,17 @@ function handleResolverResult( const sourcePropValue = resolverResult[responseKey]; if (sourcePropValue != null || existingPropValue == null) { if (existingPropValue != null && typeof existingPropValue === 'object') { - object[responseKey] = mergeDeep([existingPropValue, sourcePropValue]); + if ( + Array.isArray(existingPropValue) && + Array.isArray(sourcePropValue) && + existingPropValue.length === sourcePropValue.length + ) { + object[responseKey] = existingPropValue.map((existingElement, index) => + mergeDeep([existingElement, sourcePropValue[index]]), + ); + } else { + object[responseKey] = mergeDeep([existingPropValue, sourcePropValue]); + } } else { object[responseKey] = sourcePropValue; } diff --git a/packages/federation/src/supergraph.ts b/packages/federation/src/supergraph.ts index 12c24929b68..89133acf80d 100644 --- a/packages/federation/src/supergraph.ts +++ b/packages/federation/src/supergraph.ts @@ -692,12 +692,14 @@ export function getSubschemasFromSupergraphSdl({ let executor: Executor = onExecutor({ subgraphName, endpoint, subgraphSchema: schema }); if (globalThis.process?.env?.['DEBUG']) { const origExecutor = executor; - executor = function debugExecutor(execReq) { + executor = async function debugExecutor(execReq) { console.log(`Executing ${subgraphName} with args:`, { document: print(execReq.document), - variables: execReq.variables, + variables: JSON.stringify(execReq.variables, null, 2), }); - return origExecutor(execReq); + const res = await origExecutor(execReq); + console.log(`Response from ${subgraphName}:`, JSON.stringify(res, null, 2)); + return res; }; } subschemaMap.set(subgraphName, { diff --git a/packages/federation/test/.gitignore b/packages/federation/test/.gitignore new file mode 100644 index 00000000000..2f7fca3ee4c --- /dev/null +++ b/packages/federation/test/.gitignore @@ -0,0 +1 @@ +ignored-hidden diff --git a/packages/stitch/src/getFieldsNotInSubschema.ts b/packages/stitch/src/getFieldsNotInSubschema.ts index 79308e11546..1e8eb81d9a7 100644 --- a/packages/stitch/src/getFieldsNotInSubschema.ts +++ b/packages/stitch/src/getFieldsNotInSubschema.ts @@ -82,8 +82,13 @@ export function extractUnavailableFields(field: GraphQLField, fieldNod } const subFields = fieldType.getFields(); const unavailableSelections: SelectionNode[] = []; + let hasTypeName = false; for (const selection of fieldNode.selectionSet.selections) { if (selection.kind === Kind.FIELD) { + if (selection.name.value === '__typename') { + hasTypeName = true; + continue; + } const selectionField = subFields[selection.name.value]; if (!selectionField) { unavailableSelections.push(selection); @@ -103,6 +108,15 @@ export function extractUnavailableFields(field: GraphQLField, fieldNod // TODO: Support for inline fragments } } + if (unavailableSelections.length && hasTypeName) { + unavailableSelections.unshift({ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }); + } return unavailableSelections; } return []; diff --git a/packages/stitch/tests/extractUnavailableFields.test.ts b/packages/stitch/tests/extractUnavailableFields.test.ts new file mode 100644 index 00000000000..0b9ac5a534e --- /dev/null +++ b/packages/stitch/tests/extractUnavailableFields.test.ts @@ -0,0 +1,112 @@ +import { getOperationAST, isObjectType, Kind, parse, print, SelectionSetNode } from 'graphql'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { stripWhitespaces } from '../../merge/tests/utils'; +import { extractUnavailableFields } from '../src/getFieldsNotInSubschema'; + +describe('extractUnavailableFields', () => { + it('should extract correct fields', () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + user: User + } + type User { + id: ID! + name: String! + } + `, + }); + const userQuery = /* GraphQL */ ` + query { + user { + id + name + email + friends { + id + name + email + } + } + } + `; + const userQueryDoc = parse(userQuery, { noLocation: true }); + const operationAst = getOperationAST(userQueryDoc, null); + if (!operationAst) { + throw new Error('Operation AST not found'); + } + const selectionSet = operationAst.selectionSet; + const userSelection = selectionSet.selections[0]; + if (userSelection.kind !== 'Field') { + throw new Error('User selection not found'); + } + const queryType = schema.getType('Query'); + if (!isObjectType(queryType)) { + throw new Error('Query type not found'); + } + const userField = queryType.getFields()['user']; + if (!userField) { + throw new Error('User field not found'); + } + const unavailableFields = extractUnavailableFields(userField, userSelection); + const extractedSelectionSet: SelectionSetNode = { + kind: Kind.SELECTION_SET, + selections: unavailableFields, + }; + expect(stripWhitespaces(print(extractedSelectionSet))).toBe( + `{ email friends { id name email } }`, + ); + }); + it('excludes the fields only with __typename', () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + user: User + } + type User { + id: ID! + name: String! + friends: [User] + } + `, + }); + const userQuery = /* GraphQL */ ` + query { + user { + __typename + id + name + friends { + __typename + id + name + } + } + } + `; + const userQueryDoc = parse(userQuery, { noLocation: true }); + const operationAst = getOperationAST(userQueryDoc, null); + if (!operationAst) { + throw new Error('Operation AST not found'); + } + const selectionSet = operationAst.selectionSet; + const userSelection = selectionSet.selections[0]; + if (userSelection.kind !== 'Field') { + throw new Error('User selection not found'); + } + const queryType = schema.getType('Query'); + if (!isObjectType(queryType)) { + throw new Error('Query type not found'); + } + const userField = queryType.getFields()['user']; + if (!userField) { + throw new Error('User field not found'); + } + const unavailableFields = extractUnavailableFields(userField, userSelection); + const extractedSelectionSet: SelectionSetNode = { + kind: Kind.SELECTION_SET, + selections: unavailableFields, + }; + expect(stripWhitespaces(print(extractedSelectionSet))).toBe(''); + }); +});