Skip to content

Commit

Permalink
Merge list fields correctly (#6109)
Browse files Browse the repository at this point in the history
* Merge list fields correctly

* Changesets

* Tests
  • Loading branch information
ardatan committed Apr 29, 2024
1 parent 663130d commit 074fad4
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/brown-beers-relax.md
@@ -0,0 +1,5 @@
---
"@graphql-tools/federation": patch
---

Show responses in debug logging with `DEBUG` env var
5 changes: 5 additions & 0 deletions .changeset/red-balloons-cheer.md
@@ -0,0 +1,5 @@
---
"@graphql-tools/delegate": patch
---

Merge list fields correctly
5 changes: 5 additions & 0 deletions .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
12 changes: 11 additions & 1 deletion packages/delegate/src/mergeFields.ts
Expand Up @@ -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;
}
Expand Down
8 changes: 5 additions & 3 deletions packages/federation/src/supergraph.ts
Expand Up @@ -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, {
Expand Down
1 change: 1 addition & 0 deletions packages/federation/test/.gitignore
@@ -0,0 +1 @@
ignored-hidden
14 changes: 14 additions & 0 deletions packages/stitch/src/getFieldsNotInSubschema.ts
Expand Up @@ -82,8 +82,13 @@ export function extractUnavailableFields(field: GraphQLField<any, any>, 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);
Expand All @@ -103,6 +108,15 @@ export function extractUnavailableFields(field: GraphQLField<any, any>, fieldNod
// TODO: Support for inline fragments
}
}
if (unavailableSelections.length && hasTypeName) {
unavailableSelections.unshift({
kind: Kind.FIELD,
name: {
kind: Kind.NAME,
value: '__typename',
},
});
}
return unavailableSelections;
}
return [];
Expand Down
112 changes: 112 additions & 0 deletions 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('');
});
});

0 comments on commit 074fad4

Please sign in to comment.