@apollo/gateway 2.3.0-beta.1 → 2.3.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/executeQueryPlan.d.ts +6 -2
- package/dist/executeQueryPlan.d.ts.map +1 -1
- package/dist/executeQueryPlan.js +141 -37
- package/dist/executeQueryPlan.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -17
- package/dist/index.js.map +1 -1
- package/dist/resultShaping.d.ts +12 -0
- package/dist/resultShaping.d.ts.map +1 -0
- package/dist/resultShaping.js +227 -0
- package/dist/resultShaping.js.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/executeQueryPlan.test.ts +358 -64
- package/src/__tests__/execution-utils.ts +1 -0
- package/src/__tests__/resultShaping.test.ts +467 -0
- package/src/executeQueryPlan.ts +251 -61
- package/src/index.ts +4 -28
- package/src/resultShaping.ts +439 -0
- package/src/utilities/__tests__/cleanErrorOfInaccessibleElements.test.ts +0 -107
- package/src/utilities/cleanErrorOfInaccessibleNames.ts +0 -29
|
@@ -50,8 +50,9 @@ describe('executeQueryPlan', () => {
|
|
|
50
50
|
executeServiceMap?: { [serviceName: string]: LocalGraphQLDataSource }
|
|
51
51
|
): Promise<GatewayExecutionResult> {
|
|
52
52
|
const supergraphSchema = executeSchema ?? schema;
|
|
53
|
+
const apiSchema = supergraphSchema.toAPISchema();
|
|
53
54
|
const operationContext = buildOperationContext({
|
|
54
|
-
schema:
|
|
55
|
+
schema: apiSchema.toGraphQLJSSchema(),
|
|
55
56
|
operationDocument: gql`${operation.toString()}`,
|
|
56
57
|
});
|
|
57
58
|
return executeQueryPlan(
|
|
@@ -60,6 +61,7 @@ describe('executeQueryPlan', () => {
|
|
|
60
61
|
executeRequestContext ?? buildRequestContext(),
|
|
61
62
|
operationContext,
|
|
62
63
|
supergraphSchema.toGraphQLJSSchema(),
|
|
64
|
+
apiSchema,
|
|
63
65
|
);
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -151,7 +153,7 @@ describe('executeQueryPlan', () => {
|
|
|
151
153
|
'errors.0.message',
|
|
152
154
|
'Something went wrong',
|
|
153
155
|
);
|
|
154
|
-
expect(response).toHaveProperty('errors.0.path',
|
|
156
|
+
expect(response).toHaveProperty('errors.0.path', ["me"]);
|
|
155
157
|
expect(response).toHaveProperty(
|
|
156
158
|
'errors.0.extensions.code',
|
|
157
159
|
'UNAUTHENTICATED',
|
|
@@ -164,6 +166,315 @@ describe('executeQueryPlan', () => {
|
|
|
164
166
|
expect(response).not.toHaveProperty('errors.0.extensions.variables');
|
|
165
167
|
});
|
|
166
168
|
|
|
169
|
+
it(`error paths in joins`, async () => {
|
|
170
|
+
const s1 = {
|
|
171
|
+
name: 'S1',
|
|
172
|
+
typeDefs: gql`
|
|
173
|
+
type Query {
|
|
174
|
+
getA: A
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
type A @key(fields: "id") {
|
|
178
|
+
id: ID!
|
|
179
|
+
}
|
|
180
|
+
`,
|
|
181
|
+
resolvers: {
|
|
182
|
+
Query: {
|
|
183
|
+
getA() {
|
|
184
|
+
return { id: '1' };
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const s2 = {
|
|
191
|
+
name: 'S2',
|
|
192
|
+
typeDefs: gql`
|
|
193
|
+
type A @key(fields: "id") {
|
|
194
|
+
id: ID!
|
|
195
|
+
b: Int
|
|
196
|
+
c: [D]
|
|
197
|
+
g: Int! # will return null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
type D @key(fields: "id") {
|
|
201
|
+
id: ID!
|
|
202
|
+
}
|
|
203
|
+
`,
|
|
204
|
+
resolvers: {
|
|
205
|
+
A: {
|
|
206
|
+
b() {
|
|
207
|
+
throw new GraphQLError('Something went wrong');
|
|
208
|
+
},
|
|
209
|
+
c() {
|
|
210
|
+
return [{ id: 'd1' }, { id: 'd2' }];
|
|
211
|
+
},
|
|
212
|
+
g() {
|
|
213
|
+
return null;
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const s3 = {
|
|
220
|
+
name: 'S3',
|
|
221
|
+
typeDefs: gql`
|
|
222
|
+
type D @key(fields: "id") {
|
|
223
|
+
id: ID!
|
|
224
|
+
e: Int
|
|
225
|
+
f: [A]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
type A @key(fields: "id") {
|
|
229
|
+
id: ID!
|
|
230
|
+
}
|
|
231
|
+
`,
|
|
232
|
+
resolvers: {
|
|
233
|
+
D: {
|
|
234
|
+
e() {
|
|
235
|
+
throw new GraphQLError('Something went wrong');
|
|
236
|
+
},
|
|
237
|
+
f() {
|
|
238
|
+
return [{ id: 'a' }];
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([
|
|
245
|
+
s1,
|
|
246
|
+
s2,
|
|
247
|
+
s3,
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
const operation = parseOp(
|
|
251
|
+
`
|
|
252
|
+
query {
|
|
253
|
+
getA {
|
|
254
|
+
b
|
|
255
|
+
c {
|
|
256
|
+
e
|
|
257
|
+
f {
|
|
258
|
+
g
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
`,
|
|
264
|
+
schema,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const queryPlan = buildPlan(operation, queryPlanner);
|
|
268
|
+
|
|
269
|
+
const response = await executePlan(
|
|
270
|
+
queryPlan,
|
|
271
|
+
operation,
|
|
272
|
+
undefined,
|
|
273
|
+
schema,
|
|
274
|
+
serviceMap,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const errors = response?.errors?.map((e) => e.toJSON());
|
|
278
|
+
|
|
279
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
280
|
+
Array [
|
|
281
|
+
Object {
|
|
282
|
+
"extensions": Object {
|
|
283
|
+
"code": "DOWNSTREAM_SERVICE_ERROR",
|
|
284
|
+
"serviceName": "S2",
|
|
285
|
+
},
|
|
286
|
+
"message": "Something went wrong",
|
|
287
|
+
"path": Array [
|
|
288
|
+
"getA",
|
|
289
|
+
"b",
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
Object {
|
|
293
|
+
"extensions": Object {
|
|
294
|
+
"code": "DOWNSTREAM_SERVICE_ERROR",
|
|
295
|
+
"serviceName": "S3",
|
|
296
|
+
},
|
|
297
|
+
"message": "Something went wrong",
|
|
298
|
+
"path": Array [
|
|
299
|
+
"getA",
|
|
300
|
+
"c",
|
|
301
|
+
0,
|
|
302
|
+
"e",
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
Object {
|
|
306
|
+
"extensions": Object {
|
|
307
|
+
"code": "DOWNSTREAM_SERVICE_ERROR",
|
|
308
|
+
"serviceName": "S3",
|
|
309
|
+
},
|
|
310
|
+
"message": "Something went wrong",
|
|
311
|
+
"path": Array [
|
|
312
|
+
"getA",
|
|
313
|
+
"c",
|
|
314
|
+
1,
|
|
315
|
+
"e",
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
Object {
|
|
319
|
+
"extensions": Object {
|
|
320
|
+
"code": "DOWNSTREAM_SERVICE_ERROR",
|
|
321
|
+
"serviceName": "S2",
|
|
322
|
+
},
|
|
323
|
+
"message": "Cannot return null for non-nullable field A.g.",
|
|
324
|
+
"path": Array [
|
|
325
|
+
"getA",
|
|
326
|
+
"c",
|
|
327
|
+
0,
|
|
328
|
+
"f",
|
|
329
|
+
0,
|
|
330
|
+
"g",
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
Object {
|
|
334
|
+
"extensions": Object {
|
|
335
|
+
"code": "DOWNSTREAM_SERVICE_ERROR",
|
|
336
|
+
"serviceName": "S2",
|
|
337
|
+
},
|
|
338
|
+
"message": "Cannot return null for non-nullable field A.g.",
|
|
339
|
+
"path": Array [
|
|
340
|
+
"getA",
|
|
341
|
+
"c",
|
|
342
|
+
1,
|
|
343
|
+
"f",
|
|
344
|
+
0,
|
|
345
|
+
"g",
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
]
|
|
349
|
+
`);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it(`error paths in joins, re-entering through Query`, async () => {
|
|
353
|
+
const s1 = {
|
|
354
|
+
name: 'S1',
|
|
355
|
+
typeDefs: gql`
|
|
356
|
+
type Query {
|
|
357
|
+
a: A
|
|
358
|
+
d: String
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
type A @key(fields: "id") {
|
|
362
|
+
id: ID!
|
|
363
|
+
}
|
|
364
|
+
`,
|
|
365
|
+
resolvers: {
|
|
366
|
+
Query: {
|
|
367
|
+
a() {
|
|
368
|
+
return { id: '1' };
|
|
369
|
+
},
|
|
370
|
+
d: () => {
|
|
371
|
+
throw new GraphQLError('d error');
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const s2 = {
|
|
378
|
+
name: 'S2',
|
|
379
|
+
typeDefs: gql`
|
|
380
|
+
type A @key(fields: "id") {
|
|
381
|
+
id: ID!
|
|
382
|
+
b: String
|
|
383
|
+
q: Query
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
type Query {
|
|
387
|
+
c: String
|
|
388
|
+
}
|
|
389
|
+
`,
|
|
390
|
+
resolvers: {
|
|
391
|
+
A: {
|
|
392
|
+
b: () => {
|
|
393
|
+
throw new GraphQLError('b error');
|
|
394
|
+
},
|
|
395
|
+
q: () => ({}),
|
|
396
|
+
},
|
|
397
|
+
Query: {
|
|
398
|
+
c: () => {
|
|
399
|
+
throw new GraphQLError('c error');
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([
|
|
406
|
+
s1,
|
|
407
|
+
s2,
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
const operation = parseOp(
|
|
411
|
+
`
|
|
412
|
+
query {
|
|
413
|
+
a {
|
|
414
|
+
b
|
|
415
|
+
q {
|
|
416
|
+
c
|
|
417
|
+
d
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
`,
|
|
422
|
+
schema,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const queryPlan = buildPlan(operation, queryPlanner);
|
|
426
|
+
|
|
427
|
+
const response = await executePlan(
|
|
428
|
+
queryPlan,
|
|
429
|
+
operation,
|
|
430
|
+
undefined,
|
|
431
|
+
schema,
|
|
432
|
+
serviceMap,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const errors = response?.errors?.map((e) => e.toJSON());
|
|
436
|
+
|
|
437
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
438
|
+
Array [
|
|
439
|
+
Object {
|
|
440
|
+
"extensions": Object {
|
|
441
|
+
"code": "DOWNSTREAM_SERVICE_ERROR",
|
|
442
|
+
"serviceName": "S2",
|
|
443
|
+
},
|
|
444
|
+
"message": "b error",
|
|
445
|
+
"path": Array [
|
|
446
|
+
"a",
|
|
447
|
+
"b",
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
Object {
|
|
451
|
+
"extensions": Object {
|
|
452
|
+
"code": "DOWNSTREAM_SERVICE_ERROR",
|
|
453
|
+
"serviceName": "S2",
|
|
454
|
+
},
|
|
455
|
+
"message": "c error",
|
|
456
|
+
"path": Array [
|
|
457
|
+
"a",
|
|
458
|
+
"q",
|
|
459
|
+
"c",
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
Object {
|
|
463
|
+
"extensions": Object {
|
|
464
|
+
"code": "DOWNSTREAM_SERVICE_ERROR",
|
|
465
|
+
"serviceName": "S1",
|
|
466
|
+
},
|
|
467
|
+
"message": "d error",
|
|
468
|
+
"path": Array [
|
|
469
|
+
"a",
|
|
470
|
+
"q",
|
|
471
|
+
"d",
|
|
472
|
+
],
|
|
473
|
+
},
|
|
474
|
+
]
|
|
475
|
+
`);
|
|
476
|
+
});
|
|
477
|
+
|
|
167
478
|
it(`should not send request to downstream services when all entities are undefined`, async () => {
|
|
168
479
|
const accountsEntitiesResolverSpy =
|
|
169
480
|
spyOnEntitiesResolverInService('accounts');
|
|
@@ -996,42 +1307,11 @@ describe('executeQueryPlan', () => {
|
|
|
996
1307
|
const queryPlan = queryPlanner.buildQueryPlan(operation);
|
|
997
1308
|
|
|
998
1309
|
const response = await executePlan(queryPlan, operation, undefined, schema);
|
|
999
|
-
|
|
1000
|
-
expect(response.
|
|
1001
|
-
|
|
1002
|
-
"
|
|
1003
|
-
|
|
1004
|
-
"author": Object {
|
|
1005
|
-
"username": "@ada",
|
|
1006
|
-
},
|
|
1007
|
-
"body": "Love it!",
|
|
1008
|
-
},
|
|
1009
|
-
Object {
|
|
1010
|
-
"author": Object {
|
|
1011
|
-
"username": "@ada",
|
|
1012
|
-
},
|
|
1013
|
-
"body": "Too expensive.",
|
|
1014
|
-
},
|
|
1015
|
-
Object {
|
|
1016
|
-
"author": Object {
|
|
1017
|
-
"username": "@complete",
|
|
1018
|
-
},
|
|
1019
|
-
"body": "Could be better.",
|
|
1020
|
-
},
|
|
1021
|
-
Object {
|
|
1022
|
-
"author": Object {
|
|
1023
|
-
"username": "@complete",
|
|
1024
|
-
},
|
|
1025
|
-
"body": "Prefer something else.",
|
|
1026
|
-
},
|
|
1027
|
-
Object {
|
|
1028
|
-
"author": Object {
|
|
1029
|
-
"username": "@complete",
|
|
1030
|
-
},
|
|
1031
|
-
"body": "Wish I had read this before.",
|
|
1032
|
-
},
|
|
1033
|
-
],
|
|
1034
|
-
}
|
|
1310
|
+
expect(response.data).toBeUndefined();
|
|
1311
|
+
expect(response.errors).toMatchInlineSnapshot(`
|
|
1312
|
+
Array [
|
|
1313
|
+
[GraphQLError: Cannot query field "ssn" on type "User".],
|
|
1314
|
+
]
|
|
1035
1315
|
`);
|
|
1036
1316
|
});
|
|
1037
1317
|
|
|
@@ -1076,23 +1356,6 @@ describe('executeQueryPlan', () => {
|
|
|
1076
1356
|
`);
|
|
1077
1357
|
});
|
|
1078
1358
|
|
|
1079
|
-
// THIS TEST SHOULD BE MODIFIED AFTER THE ISSUE OUTLINED IN
|
|
1080
|
-
// https://github.com/apollographql/federation/issues/981 HAS BEEN RESOLVED.
|
|
1081
|
-
// IT IS BEING LEFT HERE AS A TEST THAT WILL INTENTIONALLY FAIL WHEN
|
|
1082
|
-
// IT IS RESOLVED IF IT'S NOT ADDRESSED.
|
|
1083
|
-
//
|
|
1084
|
-
// This test became relevant after a combination of two things:
|
|
1085
|
-
// 1. when the gateway started surfacing errors from subgraphs happened in
|
|
1086
|
-
// https://github.com/apollographql/federation/pull/159
|
|
1087
|
-
// 2. the idea of field redaction became necessary after
|
|
1088
|
-
// https://github.com/apollographql/federation/pull/893,
|
|
1089
|
-
// which introduced the notion of inaccessible fields.
|
|
1090
|
-
// The redaction started in
|
|
1091
|
-
// https://github.com/apollographql/federation/issues/974, which added
|
|
1092
|
-
// the following test.
|
|
1093
|
-
//
|
|
1094
|
-
// However, the error surfacing (first, above) needed to be reverted, thus
|
|
1095
|
-
// de-necessitating this redaction logic which is no longer tested.
|
|
1096
1359
|
it(`doesn't leak @inaccessible typenames in error messages`, async () => {
|
|
1097
1360
|
const operationString = `#graphql
|
|
1098
1361
|
query {
|
|
@@ -1114,13 +1377,11 @@ describe('executeQueryPlan', () => {
|
|
|
1114
1377
|
const response = await executePlan(queryPlan, operation, undefined, schema);
|
|
1115
1378
|
|
|
1116
1379
|
expect(response.data?.vehicle).toEqual(null);
|
|
1117
|
-
expect(response.errors).
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
// ]
|
|
1123
|
-
// `);
|
|
1380
|
+
expect(response.errors).toMatchInlineSnapshot(`
|
|
1381
|
+
Array [
|
|
1382
|
+
[GraphQLError: Invalid __typename found for object at field Query.vehicle.],
|
|
1383
|
+
]
|
|
1384
|
+
`);
|
|
1124
1385
|
});
|
|
1125
1386
|
});
|
|
1126
1387
|
|
|
@@ -3350,7 +3611,6 @@ describe('executeQueryPlan', () => {
|
|
|
3350
3611
|
`);
|
|
3351
3612
|
|
|
3352
3613
|
const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
|
|
3353
|
-
// `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response.
|
|
3354
3614
|
expect(response.data).toMatchInlineSnapshot(`
|
|
3355
3615
|
Object {
|
|
3356
3616
|
"getT1s": Array [
|
|
@@ -3934,6 +4194,41 @@ describe('executeQueryPlan', () => {
|
|
|
3934
4194
|
`);
|
|
3935
4195
|
}
|
|
3936
4196
|
});
|
|
4197
|
+
|
|
4198
|
+
test('handles querying only the @interfaceObject', async () => {
|
|
4199
|
+
// The point of this test is that we don't want the interface to be resolved, so we don't need
|
|
4200
|
+
// any specific extra resolving.
|
|
4201
|
+
const tester = defineSchema({});
|
|
4202
|
+
|
|
4203
|
+
let { plan, response } = await tester(`
|
|
4204
|
+
query {
|
|
4205
|
+
iFromS2 {
|
|
4206
|
+
y
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
`);
|
|
4210
|
+
|
|
4211
|
+
expect(plan).toMatchInlineSnapshot(`
|
|
4212
|
+
QueryPlan {
|
|
4213
|
+
Fetch(service: "S2") {
|
|
4214
|
+
{
|
|
4215
|
+
iFromS2 {
|
|
4216
|
+
y
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
},
|
|
4220
|
+
}
|
|
4221
|
+
`);
|
|
4222
|
+
|
|
4223
|
+
expect(response.errors).toBeUndefined();
|
|
4224
|
+
expect(response.data).toMatchInlineSnapshot(`
|
|
4225
|
+
Object {
|
|
4226
|
+
"iFromS2": Object {
|
|
4227
|
+
"y": 20,
|
|
4228
|
+
},
|
|
4229
|
+
}
|
|
4230
|
+
`);
|
|
4231
|
+
});
|
|
3937
4232
|
});
|
|
3938
4233
|
|
|
3939
4234
|
describe('fields with conflicting types needing aliasing', () => {
|
|
@@ -4710,7 +5005,6 @@ describe('executeQueryPlan', () => {
|
|
|
4710
5005
|
}
|
|
4711
5006
|
}
|
|
4712
5007
|
`, schema);
|
|
4713
|
-
global.console = require('console')
|
|
4714
5008
|
const queryPlan = buildPlan(operation, queryPlanner);
|
|
4715
5009
|
expect(queryPlan).toMatchInlineSnapshot(`
|
|
4716
5010
|
QueryPlan {
|