@apollo/gateway 0.42.2 → 0.44.0

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.
@@ -7,7 +7,7 @@ import {
7
7
  } from 'graphql';
8
8
  import { addResolversToSchema, GraphQLResolverMap } from 'apollo-graphql';
9
9
  import gql from 'graphql-tag';
10
- import { GraphQLRequestContext } from 'apollo-server-types';
10
+ import { GraphQLRequestContext, VariableValues } from 'apollo-server-types';
11
11
  import { AuthenticationError } from 'apollo-server-core';
12
12
  import { buildOperationContext } from '../operationContext';
13
13
  import { executeQueryPlan } from '../executeQueryPlan';
@@ -26,10 +26,6 @@ expect.addSnapshotSerializer(astSerializer);
26
26
  expect.addSnapshotSerializer(queryPlanSerializer);
27
27
 
28
28
  describe('executeQueryPlan', () => {
29
- let serviceMap: {
30
- [serviceName: string]: LocalGraphQLDataSource;
31
- };
32
-
33
29
  function overrideResolversInService(
34
30
  serviceName: string,
35
31
  resolvers: GraphQLResolverMap,
@@ -44,6 +40,9 @@ describe('executeQueryPlan', () => {
44
40
  return jest.spyOn(entitiesField, 'resolve');
45
41
  }
46
42
 
43
+ let serviceMap: {
44
+ [serviceName: string]: LocalGraphQLDataSource;
45
+ };
47
46
  let schema: GraphQLSchema;
48
47
  let queryPlanner: QueryPlanner;
49
48
  beforeEach(() => {
@@ -53,13 +52,15 @@ describe('executeQueryPlan', () => {
53
52
  ).not.toThrow();
54
53
  });
55
54
 
56
- function buildRequestContext(): GraphQLRequestContext {
55
+ function buildRequestContext(
56
+ variables: VariableValues = {},
57
+ ): GraphQLRequestContext {
57
58
  // @ts-ignore
58
59
  return {
59
60
  cache: undefined as any,
60
61
  context: {},
61
62
  request: {
62
- variables: {},
63
+ variables,
63
64
  },
64
65
  };
65
66
  }
@@ -1379,4 +1380,260 @@ describe('executeQueryPlan', () => {
1379
1380
  // `);
1380
1381
  });
1381
1382
  });
1383
+
1384
+ describe('top-level @skip / @include usages', () => {
1385
+ it(`top-level skip never calls reviews service (using literals)`, async () => {
1386
+ const operationDocument = gql`
1387
+ query {
1388
+ topReviews @skip(if: true) {
1389
+ body
1390
+ }
1391
+ }
1392
+ `;
1393
+
1394
+ const operationContext = buildOperationContext({
1395
+ schema,
1396
+ operationDocument,
1397
+ });
1398
+
1399
+ const queryPlan = queryPlanner.buildQueryPlan(operationContext);
1400
+
1401
+ const response = await executeQueryPlan(
1402
+ queryPlan,
1403
+ serviceMap,
1404
+ buildRequestContext(),
1405
+ operationContext,
1406
+ );
1407
+
1408
+ expect(response.data).toMatchInlineSnapshot(`Object {}`);
1409
+ expect(queryPlan).not.toCallService('reviews');
1410
+ });
1411
+
1412
+ it(`top-level skip never calls reviews service (using variables)`, async () => {
1413
+ const operationDocument = gql`
1414
+ query TopReviews($shouldSkip: Boolean!) {
1415
+ topReviews @skip(if: $shouldSkip) {
1416
+ body
1417
+ }
1418
+ }
1419
+ `;
1420
+
1421
+ const operationContext = buildOperationContext({
1422
+ schema,
1423
+ operationDocument,
1424
+ });
1425
+
1426
+ const queryPlan = queryPlanner.buildQueryPlan(operationContext);
1427
+
1428
+ const variables = { shouldSkip: true };
1429
+ const response = await executeQueryPlan(
1430
+ queryPlan,
1431
+ serviceMap,
1432
+ buildRequestContext(variables),
1433
+ operationContext,
1434
+ );
1435
+
1436
+ expect(response.data).toMatchInlineSnapshot(`Object {}`);
1437
+ expect({ queryPlan, variables }).not.toCallService('reviews');
1438
+ });
1439
+
1440
+ it(`call to service isn't skipped unless all top-level fields are skipped`, async () => {
1441
+ const operationDocument = gql`
1442
+ query {
1443
+ user(id: "1") @skip(if: true) {
1444
+ username
1445
+ }
1446
+ me @include(if: true) {
1447
+ username
1448
+ }
1449
+ }
1450
+ `;
1451
+
1452
+ const operationContext = buildOperationContext({
1453
+ schema,
1454
+ operationDocument,
1455
+ });
1456
+
1457
+ const queryPlan = queryPlanner.buildQueryPlan(operationContext);
1458
+
1459
+ const response = await executeQueryPlan(
1460
+ queryPlan,
1461
+ serviceMap,
1462
+ buildRequestContext(),
1463
+ operationContext,
1464
+ );
1465
+
1466
+ expect(response.data).toMatchObject({
1467
+ me: {
1468
+ username: '@ada',
1469
+ },
1470
+ });
1471
+ expect(queryPlan).toCallService('accounts');
1472
+ });
1473
+
1474
+ it(`call to service is skipped when all top-level fields are skipped`, async () => {
1475
+ const operationDocument = gql`
1476
+ query {
1477
+ user(id: "1") @skip(if: true) {
1478
+ username
1479
+ }
1480
+ me @include(if: false) {
1481
+ username
1482
+ }
1483
+ }
1484
+ `;
1485
+
1486
+ const operationContext = buildOperationContext({
1487
+ schema,
1488
+ operationDocument,
1489
+ });
1490
+
1491
+ const queryPlan = queryPlanner.buildQueryPlan(operationContext);
1492
+
1493
+ const response = await executeQueryPlan(
1494
+ queryPlan,
1495
+ serviceMap,
1496
+ buildRequestContext(),
1497
+ operationContext,
1498
+ );
1499
+
1500
+ expect(response.data).toMatchObject({});
1501
+ expect(queryPlan).not.toCallService('accounts');
1502
+ });
1503
+
1504
+ describe('@skip and @include combinations', () => {
1505
+ it(`include: false, skip: false`, async () => {
1506
+ const operationDocument = gql`
1507
+ query TopReviews($shouldInclude: Boolean!, $shouldSkip: Boolean!) {
1508
+ topReviews @include(if: $shouldInclude) @skip(if: $shouldSkip) {
1509
+ body
1510
+ }
1511
+ }
1512
+ `;
1513
+
1514
+ const operationContext = buildOperationContext({
1515
+ schema,
1516
+ operationDocument,
1517
+ });
1518
+
1519
+ const queryPlan = queryPlanner.buildQueryPlan(operationContext);
1520
+
1521
+ const variables = { shouldInclude: false, shouldSkip: false };
1522
+ const response = await executeQueryPlan(
1523
+ queryPlan,
1524
+ serviceMap,
1525
+ buildRequestContext(variables),
1526
+ operationContext,
1527
+ );
1528
+
1529
+ expect(response.data).toMatchObject({});
1530
+ expect({ queryPlan, variables }).not.toCallService('reviews');
1531
+ });
1532
+
1533
+ it(`include: false, skip: true`, async () => {
1534
+ const operationDocument = gql`
1535
+ query TopReviews($shouldInclude: Boolean!, $shouldSkip: Boolean!) {
1536
+ topReviews @include(if: $shouldInclude) @skip(if: $shouldSkip) {
1537
+ body
1538
+ }
1539
+ }
1540
+ `;
1541
+
1542
+ const operationContext = buildOperationContext({
1543
+ schema,
1544
+ operationDocument,
1545
+ });
1546
+
1547
+ const queryPlan = queryPlanner.buildQueryPlan(operationContext);
1548
+
1549
+ const variables = { shouldInclude: false, shouldSkip: true };
1550
+ const response = await executeQueryPlan(
1551
+ queryPlan,
1552
+ serviceMap,
1553
+ buildRequestContext(variables),
1554
+ operationContext,
1555
+ );
1556
+
1557
+ expect(response.data).toMatchObject({});
1558
+ expect({ queryPlan, variables }).not.toCallService('reviews');
1559
+ });
1560
+
1561
+ it(`include: true, skip: false`, async () => {
1562
+ const operationDocument = gql`
1563
+ query TopReviews($shouldInclude: Boolean!, $shouldSkip: Boolean!) {
1564
+ topReviews(first: 2) @include(if: $shouldInclude) @skip(if: $shouldSkip) {
1565
+ body
1566
+ }
1567
+ }
1568
+ `;
1569
+
1570
+ const operationContext = buildOperationContext({
1571
+ schema,
1572
+ operationDocument,
1573
+ });
1574
+
1575
+ const queryPlan = queryPlanner.buildQueryPlan(operationContext);
1576
+
1577
+ const variables = { shouldInclude: true, shouldSkip: false };
1578
+ const response = await executeQueryPlan(
1579
+ queryPlan,
1580
+ serviceMap,
1581
+ buildRequestContext(variables),
1582
+ operationContext,
1583
+ );
1584
+
1585
+ expect(response.data).toMatchObject({
1586
+ topReviews: [
1587
+ {
1588
+ body: 'Love it!',
1589
+ },
1590
+ {
1591
+ body: 'Too expensive.',
1592
+ },
1593
+ ],
1594
+ });
1595
+ expect({ queryPlan, variables }).toCallService('reviews');
1596
+ });
1597
+
1598
+ it(`include: true, skip: true`, async () => {
1599
+ const operationDocument = gql`
1600
+ query TopReviews($shouldInclude: Boolean!, $shouldSkip: Boolean!) {
1601
+ topReviews @include(if: $shouldInclude) @skip(if: $shouldSkip) {
1602
+ body
1603
+ }
1604
+ }
1605
+ `;
1606
+
1607
+ const operationContext = buildOperationContext({
1608
+ schema,
1609
+ operationDocument,
1610
+ });
1611
+
1612
+ const queryPlan = queryPlanner.buildQueryPlan(operationContext);
1613
+
1614
+ const variables = { shouldInclude: true, shouldSkip: true };
1615
+ const response = await executeQueryPlan(
1616
+ queryPlan,
1617
+ serviceMap,
1618
+ buildRequestContext(variables),
1619
+ operationContext,
1620
+ );
1621
+
1622
+ expect(response.data).toMatchObject({});
1623
+ expect(queryPlan).toMatchInlineSnapshot(`
1624
+ QueryPlan {
1625
+ Fetch(service: "reviews", inclusionConditions: [{ include: "shouldInclude", skip: "shouldSkip" }]) {
1626
+ {
1627
+ topReviews @include(if: $shouldInclude) @skip(if: $shouldSkip) {
1628
+ body
1629
+ }
1630
+ }
1631
+ },
1632
+ }
1633
+ `);
1634
+
1635
+ expect({ queryPlan, variables }).not.toCallService('reviews');
1636
+ });
1637
+ });
1638
+ });
1382
1639
  });
@@ -110,7 +110,11 @@ export function getTestingSupergraphSdl(services: typeof fixtures = fixtures) {
110
110
  if (!compositionHasErrors(compositionResult)) {
111
111
  return compositionResult.supergraphSdl;
112
112
  }
113
- throw new Error("Testing fixtures don't compose properly!");
113
+ throw new Error(
114
+ `Testing fixtures don't compose properly!\n${compositionResult.errors.join(
115
+ '\t\n',
116
+ )}`,
117
+ );
114
118
  }
115
119
 
116
120
  export function wait(ms: number) {
@@ -13,6 +13,8 @@ import {
13
13
  mockSupergraphSdlRequest,
14
14
  mockApolloConfig,
15
15
  mockCloudConfigUrl,
16
+ mockSupergraphSdlRequestIfAfter,
17
+ mockSupergraphSdlRequestSuccessIfAfter,
16
18
  } from './nockMocks';
17
19
  import {
18
20
  accounts,
@@ -130,7 +132,11 @@ it('Fetches Supergraph SDL from remote storage using a configured env variable',
130
132
 
131
133
  it('Updates Supergraph SDL from remote storage', async () => {
132
134
  mockSupergraphSdlRequestSuccess();
133
- mockSupergraphSdlRequestSuccess(getTestingSupergraphSdl(fixturesWithUpdate), 'updatedId-5678');
135
+ mockSupergraphSdlRequestSuccessIfAfter(
136
+ 'originalId-1234',
137
+ 'updatedId-5678',
138
+ getTestingSupergraphSdl(fixturesWithUpdate),
139
+ );
134
140
 
135
141
  // This test is only interested in the second time the gateway notifies of an
136
142
  // update, since the first happens on load.
@@ -183,7 +189,7 @@ describe('Supergraph SDL update failures', () => {
183
189
 
184
190
  it('Handles arbitrary fetch failures (non 200 response)', async () => {
185
191
  mockSupergraphSdlRequestSuccess();
186
- mockSupergraphSdlRequest().reply(500);
192
+ mockSupergraphSdlRequestIfAfter('originalId-1234').reply(500);
187
193
 
188
194
  // Spy on logger.error so we can just await once it's been called
189
195
  let errorLogged: Function;
@@ -208,7 +214,7 @@ describe('Supergraph SDL update failures', () => {
208
214
 
209
215
  it('Handles GraphQL errors', async () => {
210
216
  mockSupergraphSdlRequestSuccess();
211
- mockSupergraphSdlRequest().reply(200, {
217
+ mockSupergraphSdlRequest('originalId-1234').reply(200, {
212
218
  errors: [
213
219
  {
214
220
  message: 'Cannot query field "fail" on type "Query".',
@@ -242,7 +248,7 @@ describe('Supergraph SDL update failures', () => {
242
248
 
243
249
  it("Doesn't update and logs on receiving unparseable Supergraph SDL", async () => {
244
250
  mockSupergraphSdlRequestSuccess();
245
- mockSupergraphSdlRequest().reply(
251
+ mockSupergraphSdlRequestIfAfter('originalId-1234').reply(
246
252
  200,
247
253
  JSON.stringify({
248
254
  data: {
@@ -314,8 +320,12 @@ describe('Supergraph SDL update failures', () => {
314
320
  it('Rollsback to a previous schema when triggered', async () => {
315
321
  // Init
316
322
  mockSupergraphSdlRequestSuccess();
317
- mockSupergraphSdlRequestSuccess(getTestingSupergraphSdl(fixturesWithUpdate), 'updatedId-5678');
318
- mockSupergraphSdlRequestSuccess();
323
+ mockSupergraphSdlRequestSuccessIfAfter(
324
+ 'originalId-1234',
325
+ 'updatedId-5678',
326
+ getTestingSupergraphSdl(fixturesWithUpdate),
327
+ );
328
+ mockSupergraphSdlRequestSuccessIfAfter('updatedId-5678');
319
329
 
320
330
  let firstResolve: Function;
321
331
  let secondResolve: Function;
@@ -480,9 +490,10 @@ describe('Downstream service health checks', () => {
480
490
  mockAllServicesHealthCheckSuccess();
481
491
 
482
492
  // Update
483
- mockSupergraphSdlRequestSuccess(
484
- getTestingSupergraphSdl(fixturesWithUpdate),
493
+ mockSupergraphSdlRequestSuccessIfAfter(
494
+ 'originalId-1234',
485
495
  'updatedId-5678',
496
+ getTestingSupergraphSdl(fixturesWithUpdate),
486
497
  );
487
498
  mockAllServicesHealthCheckSuccess();
488
499
 
@@ -523,9 +534,10 @@ describe('Downstream service health checks', () => {
523
534
  mockAllServicesHealthCheckSuccess();
524
535
 
525
536
  // Update (with one health check failure)
526
- mockSupergraphSdlRequestSuccess(
527
- getTestingSupergraphSdl(fixturesWithUpdate),
537
+ mockSupergraphSdlRequestSuccessIfAfter(
538
+ 'originalId-1234',
528
539
  'updatedId-5678',
540
+ getTestingSupergraphSdl(fixturesWithUpdate),
529
541
  );
530
542
  mockServiceHealthCheck(accounts).reply(500);
531
543
  mockServiceHealthCheckSuccess(books);
@@ -70,21 +70,30 @@ export const mockCloudConfigUrl =
70
70
  export const mockOutOfBandReporterUrl =
71
71
  'https://example.outofbandreporter.com/monitoring/';
72
72
 
73
- export function mockSupergraphSdlRequest() {
73
+ export function mockSupergraphSdlRequestIfAfter(ifAfter: string | null) {
74
74
  return gatewayNock(mockCloudConfigUrl).post('/', {
75
75
  query: SUPERGRAPH_SDL_QUERY,
76
76
  variables: {
77
77
  ref: graphRef,
78
78
  apiKey: apiKey,
79
+ ifAfterId: ifAfter,
79
80
  },
80
81
  });
81
82
  }
82
83
 
83
- export function mockSupergraphSdlRequestSuccess(
84
- supergraphSdl = getTestingSupergraphSdl(),
85
- id = 'originalId-1234',
84
+ export function mockSupergraphSdlRequest(ifAfter: string | null = null) {
85
+ return mockSupergraphSdlRequestIfAfter(ifAfter);
86
+ }
87
+
88
+ export function mockSupergraphSdlRequestSuccessIfAfter(
89
+ ifAfter: string | null = null,
90
+ id: string = 'originalId-1234',
91
+ supergraphSdl: string = getTestingSupergraphSdl(),
86
92
  ) {
87
- return mockSupergraphSdlRequest().reply(
93
+ if (supergraphSdl == null) {
94
+ supergraphSdl = getTestingSupergraphSdl();
95
+ }
96
+ return mockSupergraphSdlRequestIfAfter(ifAfter).reply(
88
97
  200,
89
98
  JSON.stringify({
90
99
  data: {
@@ -98,6 +107,25 @@ export function mockSupergraphSdlRequestSuccess(
98
107
  );
99
108
  }
100
109
 
110
+ export function mockSupergraphSdlRequestIfAfterUnchanged(
111
+ ifAfter: string | null = null,
112
+ ) {
113
+ return mockSupergraphSdlRequestIfAfter(ifAfter).reply(
114
+ 200,
115
+ JSON.stringify({
116
+ data: {
117
+ routerConfig: {
118
+ __typename: 'Unchanged',
119
+ },
120
+ },
121
+ }),
122
+ );
123
+ }
124
+
125
+ export function mockSupergraphSdlRequestSuccess() {
126
+ return mockSupergraphSdlRequestSuccessIfAfter(null);
127
+ }
128
+
101
129
  export function mockOutOfBandReportRequest() {
102
130
  return gatewayNock(mockOutOfBandReporterUrl).post('/', () => true);
103
131
  }
@@ -8,6 +8,7 @@ import {
8
8
  mockOutOfBandReporterUrl,
9
9
  mockOutOfBandReportRequestSuccess,
10
10
  mockSupergraphSdlRequestSuccess,
11
+ mockSupergraphSdlRequestIfAfterUnchanged,
11
12
  } from './integration/nockMocks';
12
13
  import mockedEnv from 'mocked-env';
13
14
 
@@ -30,6 +31,8 @@ describe('loadSupergraphSdlFromStorage', () => {
30
31
  apiKey,
31
32
  endpoint: mockCloudConfigUrl,
32
33
  fetcher,
34
+ compositionId: null,
35
+
33
36
  });
34
37
 
35
38
  expect(result).toMatchInlineSnapshot(`
@@ -358,6 +361,8 @@ describe('loadSupergraphSdlFromStorage', () => {
358
361
  apiKey,
359
362
  endpoint: mockCloudConfigUrl,
360
363
  fetcher,
364
+ compositionId: null,
365
+
361
366
  }),
362
367
  ).rejects.toThrowErrorMatchingInlineSnapshot(
363
368
  `"An error occurred while fetching your schema from Apollo: 200 invalid json response body at https://example.cloud-config-url.com/cloudconfig/ reason: Unexpected token I in JSON at position 0"`,
@@ -380,6 +385,7 @@ describe('loadSupergraphSdlFromStorage', () => {
380
385
  apiKey,
381
386
  endpoint: mockCloudConfigUrl,
382
387
  fetcher,
388
+ compositionId: null,
383
389
  }),
384
390
  ).rejects.toThrowError(message);
385
391
  });
@@ -394,6 +400,7 @@ describe('loadSupergraphSdlFromStorage', () => {
394
400
  apiKey,
395
401
  endpoint: mockCloudConfigUrl,
396
402
  fetcher,
403
+ compositionId: null,
397
404
  }),
398
405
  ).rejects.toThrowErrorMatchingInlineSnapshot(
399
406
  `"An error occurred while fetching your schema from Apollo: 500 Internal Server Error"`,
@@ -412,6 +419,7 @@ describe('loadSupergraphSdlFromStorage', () => {
412
419
  apiKey,
413
420
  endpoint: mockCloudConfigUrl,
414
421
  fetcher,
422
+ compositionId: null,
415
423
  }),
416
424
  ).rejects.toThrowErrorMatchingInlineSnapshot(
417
425
  `"An error occurred while fetching your schema from Apollo: 400 invalid json response body at https://example.cloud-config-url.com/cloudconfig/ reason: Unexpected end of JSON input"`,
@@ -433,6 +441,7 @@ describe('loadSupergraphSdlFromStorage', () => {
433
441
  apiKey,
434
442
  endpoint: mockCloudConfigUrl,
435
443
  fetcher,
444
+ compositionId: null,
436
445
  }),
437
446
  ).rejects.toThrowErrorMatchingInlineSnapshot(
438
447
  `"An error occurred while fetching your schema from Apollo: 400 invalid json response body at https://example.cloud-config-url.com/cloudconfig/ reason: Unexpected end of JSON input"`,
@@ -454,6 +463,7 @@ describe('loadSupergraphSdlFromStorage', () => {
454
463
  apiKey,
455
464
  endpoint: mockCloudConfigUrl,
456
465
  fetcher,
466
+ compositionId: null,
457
467
  }),
458
468
  ).rejects.toThrowErrorMatchingInlineSnapshot(
459
469
  `"An error occurred while fetching your schema from Apollo: 413 Payload Too Large"`,
@@ -475,6 +485,7 @@ describe('loadSupergraphSdlFromStorage', () => {
475
485
  apiKey,
476
486
  endpoint: mockCloudConfigUrl,
477
487
  fetcher,
488
+ compositionId: null,
478
489
  }),
479
490
  ).rejects.toThrowErrorMatchingInlineSnapshot(
480
491
  `"An error occurred while fetching your schema from Apollo: 422 Unprocessable Entity"`,
@@ -496,6 +507,7 @@ describe('loadSupergraphSdlFromStorage', () => {
496
507
  apiKey,
497
508
  endpoint: mockCloudConfigUrl,
498
509
  fetcher,
510
+ compositionId: null,
499
511
  }),
500
512
  ).rejects.toThrowErrorMatchingInlineSnapshot(
501
513
  `"An error occurred while fetching your schema from Apollo: 408 Request Timeout"`,
@@ -518,6 +530,7 @@ describe('loadSupergraphSdlFromStorage', () => {
518
530
  apiKey,
519
531
  endpoint: mockCloudConfigUrl,
520
532
  fetcher,
533
+ compositionId: null,
521
534
  }),
522
535
  ).rejects.toThrowErrorMatchingInlineSnapshot(
523
536
  `"An error occurred while fetching your schema from Apollo: 504 Gateway Timeout"`,
@@ -539,6 +552,7 @@ describe('loadSupergraphSdlFromStorage', () => {
539
552
  apiKey,
540
553
  endpoint: mockCloudConfigUrl,
541
554
  fetcher,
555
+ compositionId: null,
542
556
  }),
543
557
  ).rejects.toThrowErrorMatchingInlineSnapshot(
544
558
  `"An error occurred while fetching your schema from Apollo: request to https://example.cloud-config-url.com/cloudconfig/ failed, reason: no response"`,
@@ -560,6 +574,7 @@ describe('loadSupergraphSdlFromStorage', () => {
560
574
  apiKey,
561
575
  endpoint: mockCloudConfigUrl,
562
576
  fetcher,
577
+ compositionId: null,
563
578
  }),
564
579
  ).rejects.toThrowErrorMatchingInlineSnapshot(
565
580
  `"An error occurred while fetching your schema from Apollo: 502 Bad Gateway"`,
@@ -581,9 +596,24 @@ describe('loadSupergraphSdlFromStorage', () => {
581
596
  apiKey,
582
597
  endpoint: mockCloudConfigUrl,
583
598
  fetcher,
599
+ compositionId: null,
584
600
  }),
585
601
  ).rejects.toThrowErrorMatchingInlineSnapshot(
586
602
  `"An error occurred while fetching your schema from Apollo: 503 Service Unavailable"`,
587
603
  );
588
604
  });
605
+
606
+ it('successfully responds to SDL unchanged by returning null', async () => {
607
+ mockSupergraphSdlRequestIfAfterUnchanged("id-1234");
608
+
609
+ const fetcher = getDefaultFetcher();
610
+ const result = await loadSupergraphSdlFromStorage({
611
+ graphRef,
612
+ apiKey,
613
+ endpoint: mockCloudConfigUrl,
614
+ fetcher,
615
+ compositionId: "id-1234",
616
+ });
617
+ expect(result).toBeNull();
618
+ });
589
619
  });
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  GraphQLExecutionResult,
3
3
  GraphQLRequestContext,
4
+ VariableValues,
4
5
  } from 'apollo-server-types';
5
6
  import { Headers } from 'apollo-server-env';
6
7
  import {
@@ -232,6 +233,10 @@ async function executeNode<TContext>(
232
233
  });
233
234
  }
234
235
  case 'Fetch': {
236
+ if (shouldSkipFetchNode(node, context.requestContext.request.variables)) {
237
+ return new Trace.QueryPlanNode();
238
+ }
239
+
235
240
  const traceNode = new Trace.QueryPlanNode.FetchNode({
236
241
  serviceName: node.serviceName,
237
242
  // executeFetch will fill in the other fields if desired.
@@ -252,6 +257,31 @@ async function executeNode<TContext>(
252
257
  }
253
258
  }
254
259
 
260
+ export function shouldSkipFetchNode(
261
+ node: FetchNode,
262
+ variables: VariableValues = {},
263
+ ) {
264
+ if (!node.inclusionConditions) return false;
265
+
266
+ return node.inclusionConditions.every((conditionals) => {
267
+ function resolveConditionalValue(conditional: 'skip' | 'include') {
268
+ const conditionalType = typeof conditionals[conditional];
269
+ if (conditionalType === 'boolean') {
270
+ return conditionals[conditional] as boolean;
271
+ } else if (conditionalType === 'string') {
272
+ return variables[conditionals[conditional] as string] as boolean;
273
+ } else {
274
+ return null;
275
+ }
276
+ }
277
+
278
+ const includeValue = resolveConditionalValue('include');
279
+ const skipValue = resolveConditionalValue('skip');
280
+
281
+ return includeValue === false || skipValue === true;
282
+ });
283
+ }
284
+
255
285
  async function executeFetch<TContext>(
256
286
  context: ExecutionContext<TContext>,
257
287
  fetch: FetchNode,
package/src/index.ts CHANGED
@@ -193,6 +193,7 @@ export class ApolloGateway implements GraphQLService {
193
193
  private serviceSdlCache = new Map<string, string>();
194
194
  private warnedStates: WarnedStates = Object.create(null);
195
195
  private queryPlanner?: QueryPlanner;
196
+ private supergraphSdl?: string;
196
197
  private parsedSupergraphSdl?: DocumentNode;
197
198
  private fetcher: typeof fetch;
198
199
  private compositionId?: string;
@@ -447,6 +448,7 @@ export class ApolloGateway implements GraphQLService {
447
448
  : this.createSchemaFromSupergraphSdl(config.supergraphSdl));
448
449
  // TODO(trevor): #580 redundant parse
449
450
  this.parsedSupergraphSdl = parse(supergraphSdl);
451
+ this.supergraphSdl = supergraphSdl;
450
452
  this.updateWithSchemaAndNotify(schema, supergraphSdl, true);
451
453
  } catch (e) {
452
454
  this.state = { phase: 'failed to load' };
@@ -576,6 +578,7 @@ export class ApolloGateway implements GraphQLService {
576
578
  await this.maybePerformServiceHealthCheck(result);
577
579
 
578
580
  this.compositionId = result.id;
581
+ this.supergraphSdl = result.supergraphSdl;
579
582
  this.parsedSupergraphSdl = parsedSupergraphSdl;
580
583
 
581
584
  const { schema, supergraphSdl } = this.createSchemaFromSupergraphSdl(
@@ -979,12 +982,18 @@ export class ApolloGateway implements GraphQLService {
979
982
 
980
983
  // TODO(trevor:cloudconfig): This condition goes away completely
981
984
  if (isPrecomposedManagedConfig(config)) {
982
- return loadSupergraphSdlFromStorage({
985
+ const result = await loadSupergraphSdlFromStorage({
983
986
  graphRef: this.apolloConfig!.graphRef!,
984
987
  apiKey: this.apolloConfig!.key!,
985
988
  endpoint: this.schemaConfigDeliveryEndpoint!,
986
989
  fetcher: this.fetcher,
990
+ compositionId: this.compositionId ?? null,
987
991
  });
992
+
993
+ return result ?? {
994
+ id: this.compositionId!,
995
+ supergraphSdl: this.supergraphSdl!,
996
+ }
988
997
  } else if (isLegacyManagedConfig(config)) {
989
998
  return getServiceDefinitionsFromStorage({
990
999
  graphRef: this.apolloConfig!.graphRef!,