Skip to content

Commit 0e87805

Browse files
authoredAug 26, 2024··
Fix nested computed fields (#6469)

File tree

7 files changed

+178
-18
lines changed

7 files changed

+178
-18
lines changed
 

‎.changeset/stale-feet-rhyme.md

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@graphql-tools/federation': patch
3+
'@graphql-tools/delegate': patch
4+
---
5+
6+
Handle merged selection sets in the computed fields;
7+
8+
When a selection set for a computed field needs to be merged, resolve that required selection set fully then resolve the computed field.
9+
In the following case, the selection set for the `author` field in the `Post` type is merged with the selection set for the `authorId` field in the `Comment` type.
10+
11+
```graphql
12+
type Query {
13+
feed: [Post!]!
14+
}
15+
16+
type Post {
17+
id: ID!
18+
author: User! @computed(selectionSet: "{ comments { authorId } }")
19+
}
20+
21+
type Comment {
22+
id: ID!
23+
authorId: ID!
24+
}
25+
26+
type User {
27+
id: ID!
28+
name: String!
29+
}
30+
```
31+
32+
```graphql
33+
type Post {
34+
id: ID!
35+
comments: [Comment!]!
36+
}
37+
38+
type Comment {
39+
id: ID!
40+
}
41+
```

‎packages/delegate/src/defaultMergedResolver.ts

+119-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
defaultFieldResolver,
3+
FieldNode,
34
GraphQLResolveInfo,
45
Kind,
56
responsePathAsArray,
@@ -15,7 +16,11 @@ import {
1516
} from './mergeFields.js';
1617
import { resolveExternalValue } from './resolveExternalValue.js';
1718
import { Subschema } from './Subschema.js';
18-
import { FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols.js';
19+
import {
20+
FIELD_SUBSCHEMA_MAP_SYMBOL,
21+
OBJECT_SUBSCHEMA_SYMBOL,
22+
UNPATHED_ERRORS_SYMBOL,
23+
} from './symbols.js';
1924
import { ExternalObject, MergedTypeResolver, StitchingInfo } from './types.js';
2025

2126
/**
@@ -66,6 +71,7 @@ export function defaultMergedResolver(
6671
}
6772
const deferred = createDeferred();
6873
missingDeferredFields.set(responseKey, deferred);
74+
handleResult(parent, responseKey, context, info);
6975
return deferred.promise;
7076
}
7177
return undefined;
@@ -110,16 +116,35 @@ function handleLeftOver<TContext extends Record<string, any>>(
110116
if (stitchingInfo) {
111117
for (const possibleSubschema of leftOver.nonProxiableSubschemas) {
112118
const parentTypeName = info.parentType.name;
113-
const selectionSet =
119+
const selectionSets = new Set<SelectionSetNode>();
120+
const mainSelectionSet =
114121
stitchingInfo.mergedTypes[parentTypeName].selectionSets.get(possibleSubschema);
122+
if (mainSelectionSet) {
123+
selectionSets.add(mainSelectionSet);
124+
}
125+
for (const fieldNode of leftOver.unproxiableFieldNodes) {
126+
const fieldName = fieldNode.name.value;
127+
const fieldSelectionSet =
128+
stitchingInfo.mergedTypes[parentTypeName].fieldSelectionSets.get(possibleSubschema)?.[
129+
fieldName
130+
];
131+
if (fieldSelectionSet) {
132+
selectionSets.add(fieldSelectionSet);
133+
}
134+
}
115135
// Wait until the parent is flattened, then check if non proxiable subschemas are satisfied now,
116136
// then the deferred fields can be resolved
117-
if (selectionSet) {
137+
if (selectionSets.size) {
138+
const selectionSet: SelectionSetNode = {
139+
kind: Kind.SELECTION_SET,
140+
selections: Array.from(selectionSets).flatMap(selectionSet => selectionSet.selections),
141+
};
118142
const flattenedParent$ = flattenPromise(parent);
119143
if (isPromise(flattenedParent$)) {
120144
flattenedParent$.then(flattenedParent => {
121145
handleFlattenedParent(
122146
flattenedParent,
147+
parent,
123148
possibleSubschema,
124149
selectionSet,
125150
leftOver,
@@ -132,6 +157,7 @@ function handleLeftOver<TContext extends Record<string, any>>(
132157
} else {
133158
handleFlattenedParent(
134159
flattenedParent$,
160+
parent,
135161
possibleSubschema,
136162
selectionSet,
137163
leftOver,
@@ -148,6 +174,7 @@ function handleLeftOver<TContext extends Record<string, any>>(
148174

149175
function handleFlattenedParent<TContext extends Record<string, any>>(
150176
flattenedParent: ExternalObject,
177+
leftOverParent: ExternalObject,
151178
possibleSubschema: Subschema,
152179
selectionSet: SelectionSetNode,
153180
leftOver: DelegationPlanLeftOver,
@@ -158,7 +185,8 @@ function handleFlattenedParent<TContext extends Record<string, any>>(
158185
) {
159186
// If this subschema is satisfied now, try to resolve the deferred fields
160187
if (parentSatisfiedSelectionSet(flattenedParent, selectionSet)) {
161-
for (const [leftOverParent, missingFieldNodes] of leftOver.missingFieldsParentMap) {
188+
const missingFieldNodes = leftOver.missingFieldsParentMap.get(leftOverParent);
189+
if (missingFieldNodes) {
162190
const resolver = stitchingInfo.mergedTypes[parentTypeName].resolvers.get(possibleSubschema);
163191
if (resolver) {
164192
try {
@@ -208,6 +236,84 @@ function handleFlattenedParent<TContext extends Record<string, any>>(
208236
}
209237
}
210238
}
239+
} else {
240+
// try to resolve the missing fields
241+
for (const selectionNode of selectionSet.selections) {
242+
if (selectionNode.kind === Kind.FIELD && selectionNode.selectionSet?.selections?.length) {
243+
const responseKey = selectionNode.alias?.value ?? selectionNode.name.value;
244+
const nestedParent = flattenedParent[responseKey];
245+
const nestedSelectionSet = selectionNode.selectionSet;
246+
if (nestedParent != null) {
247+
if (!parentSatisfiedSelectionSet(nestedParent, nestedSelectionSet)) {
248+
async function handleNestedParentItem(nestedParentItem: any, fieldNode: FieldNode) {
249+
const nestedTypeName = nestedParentItem['__typename'];
250+
const sourceSubschema = getSubschema(flattenedParent, responseKey) as Subschema;
251+
if (sourceSubschema && nestedTypeName) {
252+
const delegationPlan = stitchingInfo.mergedTypes[
253+
nestedTypeName
254+
].delegationPlanBuilder(
255+
info.schema,
256+
sourceSubschema,
257+
info.variableValues,
258+
info.fragments,
259+
[fieldNode],
260+
);
261+
// Later optimize
262+
for (const delegationMap of delegationPlan) {
263+
for (const [subschema, selectionSet] of delegationMap) {
264+
const resolver =
265+
stitchingInfo.mergedTypes[nestedTypeName].resolvers.get(subschema);
266+
if (resolver) {
267+
const res = await resolver(
268+
nestedParentItem,
269+
context,
270+
info,
271+
subschema,
272+
selectionSet,
273+
info.parentType,
274+
info.parentType,
275+
);
276+
if (res) {
277+
handleResolverResult(
278+
res,
279+
subschema,
280+
selectionSet,
281+
nestedParentItem,
282+
(nestedParentItem[FIELD_SUBSCHEMA_MAP_SYMBOL] ||= new Map()),
283+
info,
284+
responsePathAsArray(info.path),
285+
(nestedParentItem[UNPATHED_ERRORS_SYMBOL] ||= []),
286+
);
287+
}
288+
}
289+
}
290+
}
291+
if (parentSatisfiedSelectionSet(nestedParent, nestedSelectionSet)) {
292+
handleFlattenedParent(
293+
flattenedParent,
294+
leftOverParent,
295+
possibleSubschema,
296+
selectionSet,
297+
leftOver,
298+
stitchingInfo,
299+
parentTypeName,
300+
context,
301+
info,
302+
);
303+
}
304+
}
305+
}
306+
if (Array.isArray(nestedParent)) {
307+
nestedParent.forEach(nestedParentItem =>
308+
handleNestedParentItem(nestedParentItem, selectionNode),
309+
);
310+
} else {
311+
handleNestedParentItem(nestedParent, selectionNode);
312+
}
313+
}
314+
}
315+
}
316+
}
211317
}
212318
}
213319

@@ -339,6 +445,15 @@ function flattenPromise<T>(data: T): Promise<T> | T {
339445
newData[key] = keyResult;
340446
}
341447
}
448+
if (data[OBJECT_SUBSCHEMA_SYMBOL]) {
449+
newData[OBJECT_SUBSCHEMA_SYMBOL] = data[OBJECT_SUBSCHEMA_SYMBOL];
450+
}
451+
if (data[FIELD_SUBSCHEMA_MAP_SYMBOL]) {
452+
newData[FIELD_SUBSCHEMA_MAP_SYMBOL] = data[FIELD_SUBSCHEMA_MAP_SYMBOL];
453+
}
454+
if (data[UNPATHED_ERRORS_SYMBOL]) {
455+
newData[UNPATHED_ERRORS_SYMBOL] = data[UNPATHED_ERRORS_SYMBOL];
456+
}
342457
if (jobs.length) {
343458
return Promise.all(jobs).then(() => newData) as Promise<T>;
344459
}

‎packages/delegate/src/mergeFields.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ export function annotateExternalObject<TContext>(
3737
subschemaMap: Record<string, GraphQLSchema | SubschemaConfig<any, any, any, Record<string, any>>>,
3838
): ExternalObject {
3939
Object.defineProperties(object, {
40-
[OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema },
41-
[FIELD_SUBSCHEMA_MAP_SYMBOL]: { value: subschemaMap },
42-
[UNPATHED_ERRORS_SYMBOL]: { value: errors },
40+
[OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema, writable: true },
41+
[FIELD_SUBSCHEMA_MAP_SYMBOL]: { value: subschemaMap, writable: true },
42+
[UNPATHED_ERRORS_SYMBOL]: { value: errors, writable: true },
4343
});
4444
return object;
4545
}

‎packages/delegate/src/resolveExternalValue.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ function reportUnpathedErrorsViaNull(unpathedErrors: Array<GraphQLError>) {
178178

179179
if (unreportedErrors.length) {
180180
if (unreportedErrors.length === 1) {
181-
return unreportedErrors[0];
181+
return locatedError(unreportedErrors[0], undefined as any, unreportedErrors[0].path as any);
182182
}
183183

184184
return new AggregateError(

‎packages/federation/src/supergraph.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -662,9 +662,12 @@ export function getStitchingOptionsFromSupergraphSdl(
662662
if (fieldsKeyMap) {
663663
const fieldsConfig: Record<string, MergedFieldConfig> = (mergedTypeConfig.fields = {});
664664
for (const [fieldName, fieldNameKey] of fieldsKeyMap) {
665-
extraKeys.add(fieldNameKey);
665+
const aliasedFieldNameKey = fieldNameKey.includes('(')
666+
? `_${fieldNameKey.split('(')[0]}: ${fieldNameKey}`
667+
: fieldNameKey;
668+
extraKeys.add(aliasedFieldNameKey);
666669
fieldsConfig[fieldName] = {
667-
selectionSet: `{ ${fieldNameKey} }`,
670+
selectionSet: `{ ${aliasedFieldNameKey} }`,
668671
computed: true,
669672
};
670673
}

‎packages/federation/src/utils.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,21 @@ export function projectDataSelectionSet(data: any, selectionSet?: SelectionSetNo
2424
};
2525
for (const selection of selectionSet.selections) {
2626
if (selection.kind === Kind.FIELD) {
27-
const key = selection.name.value;
28-
if (Object.prototype.hasOwnProperty.call(data, key)) {
29-
const projectedKeyData = projectDataSelectionSet(data[key], selection.selectionSet);
30-
if (projectedData[key]) {
27+
const fieldName = selection.name.value;
28+
const responseKey = selection.alias?.value || selection.name.value;
29+
if (Object.prototype.hasOwnProperty.call(data, responseKey)) {
30+
const projectedKeyData = projectDataSelectionSet(data[responseKey], selection.selectionSet);
31+
if (projectedData[fieldName]) {
3132
if (projectedKeyData != null && !(projectedKeyData instanceof Error)) {
32-
projectedData[key] = mergeDeep(
33-
[projectedData[key], projectedKeyData],
33+
projectedData[fieldName] = mergeDeep(
34+
[projectedData[fieldName], projectedKeyData],
3435
undefined,
3536
true,
3637
true,
3738
);
3839
}
3940
} else {
40-
projectedData[key] = projectedKeyData;
41+
projectedData[fieldName] = projectedKeyData;
4142
}
4243
}
4344
} else if (selection.kind === Kind.INLINE_FRAGMENT) {

‎packages/federation/test/federation-compatibility.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from '@graphql-tools/utils';
2222
import { getStitchedSchemaFromSupergraphSdl } from '../src/supergraph';
2323

24-
describe.skip('Federation Compatibility', () => {
24+
describe('Federation Compatibility', () => {
2525
if (!existsSync(join(__dirname, 'fixtures', 'federation-compatibility'))) {
2626
console.warn('Make sure you fetched the fixtures from the API first');
2727
it.skip('skipping tests', () => {});

0 commit comments

Comments
 (0)
Please sign in to comment.