@apollo/gateway 2.3.0-beta.2 → 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.
@@ -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: supergraphSchema.toAPISchema().toGraphQLJSSchema(),
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', undefined);
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.data).toMatchInlineSnapshot(`
1001
- Object {
1002
- "topReviews": Array [
1003
- Object {
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).toBeUndefined();
1118
- // SEE COMMENT ABOVE THIS TEST. SHOULD BE RE-ENABLED AFTER #981 IS FIXED!
1119
- // expect(response.errors).toMatchInlineSnapshot(`
1120
- // Array [
1121
- // [GraphQLError: Abstract type "Vehicle" was resolve to a type [inaccessible type] that does not exist inside schema.],
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 {
@@ -76,6 +76,7 @@ export async function execute(
76
76
  },
77
77
  operationContext,
78
78
  schema.toGraphQLJSSchema(),
79
+ apiSchema,
79
80
  );
80
81
 
81
82
  return { ...result, queryPlan };