Skip to content

Commit 074fad4

Browse files
authored
Merge list fields correctly (#6109)
* Merge list fields correctly * Changesets * Tests
1 parent 663130d commit 074fad4

File tree

8 files changed

+158
-4
lines changed

8 files changed

+158
-4
lines changed

.changeset/brown-beers-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-tools/federation": patch
3+
---
4+
5+
Show responses in debug logging with `DEBUG` env var

.changeset/red-balloons-cheer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-tools/delegate": patch
3+
---
4+
5+
Merge list fields correctly

.changeset/tall-icons-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-tools/stitch": patch
3+
---
4+
5+
Exclude fields with `__typename` while extracting missing fields for the type merging

packages/delegate/src/mergeFields.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,17 @@ function handleResolverResult(
145145
const sourcePropValue = resolverResult[responseKey];
146146
if (sourcePropValue != null || existingPropValue == null) {
147147
if (existingPropValue != null && typeof existingPropValue === 'object') {
148-
object[responseKey] = mergeDeep([existingPropValue, sourcePropValue]);
148+
if (
149+
Array.isArray(existingPropValue) &&
150+
Array.isArray(sourcePropValue) &&
151+
existingPropValue.length === sourcePropValue.length
152+
) {
153+
object[responseKey] = existingPropValue.map((existingElement, index) =>
154+
mergeDeep([existingElement, sourcePropValue[index]]),
155+
);
156+
} else {
157+
object[responseKey] = mergeDeep([existingPropValue, sourcePropValue]);
158+
}
149159
} else {
150160
object[responseKey] = sourcePropValue;
151161
}

packages/federation/src/supergraph.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -692,12 +692,14 @@ export function getSubschemasFromSupergraphSdl({
692692
let executor: Executor = onExecutor({ subgraphName, endpoint, subgraphSchema: schema });
693693
if (globalThis.process?.env?.['DEBUG']) {
694694
const origExecutor = executor;
695-
executor = function debugExecutor(execReq) {
695+
executor = async function debugExecutor(execReq) {
696696
console.log(`Executing ${subgraphName} with args:`, {
697697
document: print(execReq.document),
698-
variables: execReq.variables,
698+
variables: JSON.stringify(execReq.variables, null, 2),
699699
});
700-
return origExecutor(execReq);
700+
const res = await origExecutor(execReq);
701+
console.log(`Response from ${subgraphName}:`, JSON.stringify(res, null, 2));
702+
return res;
701703
};
702704
}
703705
subschemaMap.set(subgraphName, {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ignored-hidden

packages/stitch/src/getFieldsNotInSubschema.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,13 @@ export function extractUnavailableFields(field: GraphQLField<any, any>, fieldNod
8282
}
8383
const subFields = fieldType.getFields();
8484
const unavailableSelections: SelectionNode[] = [];
85+
let hasTypeName = false;
8586
for (const selection of fieldNode.selectionSet.selections) {
8687
if (selection.kind === Kind.FIELD) {
88+
if (selection.name.value === '__typename') {
89+
hasTypeName = true;
90+
continue;
91+
}
8792
const selectionField = subFields[selection.name.value];
8893
if (!selectionField) {
8994
unavailableSelections.push(selection);
@@ -103,6 +108,15 @@ export function extractUnavailableFields(field: GraphQLField<any, any>, fieldNod
103108
// TODO: Support for inline fragments
104109
}
105110
}
111+
if (unavailableSelections.length && hasTypeName) {
112+
unavailableSelections.unshift({
113+
kind: Kind.FIELD,
114+
name: {
115+
kind: Kind.NAME,
116+
value: '__typename',
117+
},
118+
});
119+
}
106120
return unavailableSelections;
107121
}
108122
return [];
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { getOperationAST, isObjectType, Kind, parse, print, SelectionSetNode } from 'graphql';
2+
import { makeExecutableSchema } from '@graphql-tools/schema';
3+
import { stripWhitespaces } from '../../merge/tests/utils';
4+
import { extractUnavailableFields } from '../src/getFieldsNotInSubschema';
5+
6+
describe('extractUnavailableFields', () => {
7+
it('should extract correct fields', () => {
8+
const schema = makeExecutableSchema({
9+
typeDefs: /* GraphQL */ `
10+
type Query {
11+
user: User
12+
}
13+
type User {
14+
id: ID!
15+
name: String!
16+
}
17+
`,
18+
});
19+
const userQuery = /* GraphQL */ `
20+
query {
21+
user {
22+
id
23+
name
24+
email
25+
friends {
26+
id
27+
name
28+
email
29+
}
30+
}
31+
}
32+
`;
33+
const userQueryDoc = parse(userQuery, { noLocation: true });
34+
const operationAst = getOperationAST(userQueryDoc, null);
35+
if (!operationAst) {
36+
throw new Error('Operation AST not found');
37+
}
38+
const selectionSet = operationAst.selectionSet;
39+
const userSelection = selectionSet.selections[0];
40+
if (userSelection.kind !== 'Field') {
41+
throw new Error('User selection not found');
42+
}
43+
const queryType = schema.getType('Query');
44+
if (!isObjectType(queryType)) {
45+
throw new Error('Query type not found');
46+
}
47+
const userField = queryType.getFields()['user'];
48+
if (!userField) {
49+
throw new Error('User field not found');
50+
}
51+
const unavailableFields = extractUnavailableFields(userField, userSelection);
52+
const extractedSelectionSet: SelectionSetNode = {
53+
kind: Kind.SELECTION_SET,
54+
selections: unavailableFields,
55+
};
56+
expect(stripWhitespaces(print(extractedSelectionSet))).toBe(
57+
`{ email friends { id name email } }`,
58+
);
59+
});
60+
it('excludes the fields only with __typename', () => {
61+
const schema = makeExecutableSchema({
62+
typeDefs: /* GraphQL */ `
63+
type Query {
64+
user: User
65+
}
66+
type User {
67+
id: ID!
68+
name: String!
69+
friends: [User]
70+
}
71+
`,
72+
});
73+
const userQuery = /* GraphQL */ `
74+
query {
75+
user {
76+
__typename
77+
id
78+
name
79+
friends {
80+
__typename
81+
id
82+
name
83+
}
84+
}
85+
}
86+
`;
87+
const userQueryDoc = parse(userQuery, { noLocation: true });
88+
const operationAst = getOperationAST(userQueryDoc, null);
89+
if (!operationAst) {
90+
throw new Error('Operation AST not found');
91+
}
92+
const selectionSet = operationAst.selectionSet;
93+
const userSelection = selectionSet.selections[0];
94+
if (userSelection.kind !== 'Field') {
95+
throw new Error('User selection not found');
96+
}
97+
const queryType = schema.getType('Query');
98+
if (!isObjectType(queryType)) {
99+
throw new Error('Query type not found');
100+
}
101+
const userField = queryType.getFields()['user'];
102+
if (!userField) {
103+
throw new Error('User field not found');
104+
}
105+
const unavailableFields = extractUnavailableFields(userField, userSelection);
106+
const extractedSelectionSet: SelectionSetNode = {
107+
kind: Kind.SELECTION_SET,
108+
selections: unavailableFields,
109+
};
110+
expect(stripWhitespaces(print(extractedSelectionSet))).toBe('');
111+
});
112+
});

0 commit comments

Comments
 (0)