Skip to content

Commit d61faf0

Browse files
authored
Resolve fields from other subschemas when merging types from subscriptions in additional type defs (#8740)
* full delegate when resolving subscription event * resolve actually * resolve shippingestimate * changelog * proper comments
1 parent b691997 commit d61faf0

File tree

5 files changed

+108
-21
lines changed

5 files changed

+108
-21
lines changed

.changeset/spotty-pigs-sell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-mesh/utils': patch
3+
---
4+
5+
Resolve fields from other subschemas when merging types from subscriptions in additional type defs

e2e/subscriptions-type-merging/mesh.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export const composeConfig = defineConfig({
1010
endpoint: `http://localhost:${opts.getServicePort('products')}/graphql`,
1111
}),
1212
},
13+
{
14+
sourceHandler: loadGraphQLHTTPSubgraph('inventory', {
15+
endpoint: `http://localhost:${opts.getServicePort('inventory')}/graphql`,
16+
}),
17+
},
1318
],
1419
additionalTypeDefs: /* GraphQL */ `
1520
extend schema {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { createServer } from 'node:http';
2+
import { parse } from 'graphql';
3+
import { createYoga } from 'graphql-yoga';
4+
import { buildSubgraphSchema } from '@apollo/subgraph';
5+
import { Opts } from '@e2e/opts';
6+
7+
const port = Opts(process.argv).getServicePort('inventory');
8+
9+
createServer(
10+
createYoga({
11+
schema: buildSubgraphSchema({
12+
typeDefs: parse(/* GraphQL */ `
13+
type Query {
14+
hello: String!
15+
}
16+
type Product @key(fields: "id") {
17+
id: ID! @external
18+
price: Float! @external
19+
shippingEstimate: Int @requires(fields: "price")
20+
}
21+
`),
22+
resolvers: {
23+
Query: {
24+
hello: () => 'world',
25+
},
26+
Product: {
27+
__resolveReference: ref => ({
28+
...ref,
29+
shippingEstimate: Math.floor(ref.price / 10),
30+
}),
31+
},
32+
},
33+
}),
34+
}),
35+
).listen(port, () => {
36+
console.log(`Inventory subgraph running on http://localhost:${port}/graphql`);
37+
});

e2e/subscriptions-type-merging/subscriptions-type-merging.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ afterAll(async () => {
3737

3838
it('consumes the pubsub topics and resolves the types correctly', async () => {
3939
await using products = await service('products');
40-
await using composition = await compose({ output: 'graphql', services: [products] });
40+
await using inventory = await service('inventory');
41+
await using composition = await compose({ output: 'graphql', services: [products, inventory] });
4142
await using gw = await serve({ supergraph: composition.output, env: redisEnv });
4243
const sseClient = createClient({
4344
retryAttempts: 0,
@@ -51,6 +52,7 @@ it('consumes the pubsub topics and resolves the types correctly', async () => {
5152
id
5253
name
5354
price
55+
shippingEstimate
5456
}
5557
}
5658
`,
@@ -77,6 +79,7 @@ it('consumes the pubsub topics and resolves the types correctly', async () => {
7779
id,
7880
name: 'Roomba X' + id,
7981
price: 100,
82+
shippingEstimate: 10,
8083
},
8184
},
8285
},
@@ -89,6 +92,7 @@ it('consumes the pubsub topics and resolves the types correctly', async () => {
8992
id: 'noid',
9093
name: 'Roborock 80P',
9194
price: 100,
95+
shippingEstimate: 10,
9296
},
9397
},
9498
},

packages/legacy/utils/src/resolve-additional-resolvers.ts

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ import type {
55
GraphQLResolveInfo,
66
GraphQLSchema,
77
GraphQLType,
8+
OperationTypeNode,
89
SelectionSetNode,
910
} from 'graphql';
10-
import { getNamedType, isAbstractType, isInterfaceType, isObjectType, Kind } from 'graphql';
11+
import {
12+
getNamedType,
13+
GraphQLList,
14+
isAbstractType,
15+
isInterfaceType,
16+
isObjectType,
17+
Kind,
18+
} from 'graphql';
1119
import lodashGet from 'lodash.get';
1220
import toPath from 'lodash.topath';
1321
import { process } from '@graphql-mesh/cross-helpers';
@@ -20,9 +28,11 @@ import {
2028
type MeshPubSub,
2129
type YamlConfig,
2230
} from '@graphql-mesh/types';
31+
import { batchDelegateToSchema } from '@graphql-tools/batch-delegate';
2332
import {
33+
delegateToSchema,
2434
subtractSelectionSets,
25-
type MergedTypeResolver,
35+
type MergedTypeConfig,
2636
type StitchingInfo,
2737
type Subschema,
2838
} from '@graphql-tools/delegate';
@@ -224,7 +234,7 @@ export function resolveAdditionalResolversWithoutImport(
224234
return resolvePayload(payload); // no stitching, cannot be resolved anywhere else
225235
}
226236
const returnTypeName = getNamedType(info.returnType).name;
227-
const mergedTypeInfo = stitchingInfo?.mergedTypes?.[returnTypeName];
237+
const mergedTypeInfo = stitchingInfo.mergedTypes[returnTypeName];
228238
if (!mergedTypeInfo) {
229239
return resolvePayload(payload); // this type is not merged or resolvable
230240
}
@@ -244,39 +254,65 @@ export function resolveAdditionalResolversWithoutImport(
244254
return resolvePayload(payload);
245255
}
246256

247-
// find the best resolver by diffing the selection sets
248-
let resolver: MergedTypeResolver | null = null;
257+
// find the best subgraph by diffing the selection sets
249258
let subschema: Subschema | null = null;
259+
let mergedTypeConfig: MergedTypeConfig | null = null;
250260
for (const [requiredSubschema, requiredSelSet] of mergedTypeInfo.selectionSets) {
251-
const matchResolver = mergedTypeInfo?.resolvers.get(requiredSubschema);
252-
if (!matchResolver) {
253-
// the subschema has no resolvers, nothing to search for
261+
const tentativeMergedTypeConfig = requiredSubschema.merge?.[returnTypeName];
262+
if (tentativeMergedTypeConfig?.fields) {
263+
// this resolver requires additional fields (think `@requires(fields: "x")`)
264+
// TODO: actually implement whether the payload already contains those fields
265+
// TODO: is there a better way for finding a match?
254266
continue;
255267
}
256268
const diff = subtractSelectionSets(requiredSelSet, availableSelSet);
257269
if (!diff.selections.length) {
258270
// all of the fields of the requesting (available) selection set is exist in the required selection set
259-
resolver = matchResolver;
260271
subschema = requiredSubschema;
272+
mergedTypeConfig = tentativeMergedTypeConfig;
261273
break;
262274
}
263275
}
264-
if (!resolver || !subschema) {
276+
if (!subschema || !mergedTypeConfig) {
265277
// the type cannot be resolved
266278
return resolvePayload(payload);
267279
}
268280

269281
return handleMaybePromise(
270-
() =>
271-
resolver(
272-
payload,
273-
ctx,
274-
info,
275-
subschema,
276-
missingSelectionSet,
277-
undefined,
278-
info.returnType,
279-
),
282+
() => {
283+
if (mergedTypeConfig.argsFromKeys) {
284+
return batchDelegateToSchema({
285+
schema: subschema,
286+
operation: 'query' as OperationTypeNode,
287+
fieldName: mergedTypeConfig.fieldName,
288+
returnType: new GraphQLList(info.returnType),
289+
key: mergedTypeConfig.key?.(payload) || payload, // TODO: should use valueFromResults on the args too?
290+
argsFromKeys: mergedTypeConfig.argsFromKeys,
291+
valuesFromResults: mergedTypeConfig.valuesFromResults,
292+
selectionSet: missingSelectionSet,
293+
context: ctx,
294+
info,
295+
dataLoaderOptions: mergedTypeConfig.dataLoaderOptions,
296+
skipTypeMerging: false, // important to be false so that fields outside this subgraph can be resolved properly
297+
});
298+
}
299+
if (mergedTypeConfig.args) {
300+
return delegateToSchema({
301+
schema: subschema,
302+
operation: 'query' as OperationTypeNode,
303+
fieldName: mergedTypeConfig.fieldName,
304+
returnType: info.returnType,
305+
args: mergedTypeConfig.args(payload), // TODO: should use valueFromResults on the args too?
306+
selectionSet: missingSelectionSet,
307+
context: ctx,
308+
info,
309+
skipTypeMerging: false, // important to be false so that fields outside this subgraph can be resolved properly
310+
});
311+
}
312+
// no way to delegate to anything, return empty - i.e. resolve just payload
313+
// should not happen though, there'll be something to use
314+
return {};
315+
},
280316
resolved => resolvePayload(mergeDeep([payload, resolved])),
281317
);
282318
},

0 commit comments

Comments
 (0)