@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.
- package/CHANGELOG.md +21 -0
- package/dist/__generated__/graphqlTypes.d.ts +9 -2
- package/dist/__generated__/graphqlTypes.d.ts.map +1 -1
- package/dist/executeQueryPlan.d.ts +3 -2
- package/dist/executeQueryPlan.d.ts.map +1 -1
- package/dist/executeQueryPlan.js +26 -1
- package/dist/executeQueryPlan.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/loadSupergraphSdlFromStorage.d.ts +4 -3
- package/dist/loadSupergraphSdlFromStorage.d.ts.map +1 -1
- package/dist/loadSupergraphSdlFromStorage.js +7 -3
- package/dist/loadSupergraphSdlFromStorage.js.map +1 -1
- package/package.json +7 -7
- package/src/__generated__/graphqlTypes.ts +9 -3
- package/src/__tests__/buildQueryPlan.test.ts +277 -26
- package/src/__tests__/executeQueryPlan.test.ts +264 -7
- package/src/__tests__/execution-utils.ts +5 -1
- package/src/__tests__/integration/networkRequests.test.ts +22 -10
- package/src/__tests__/integration/nockMocks.ts +33 -5
- package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +30 -0
- package/src/executeQueryPlan.ts +30 -0
- package/src/index.ts +10 -1
- package/src/loadSupergraphSdlFromStorage.ts +7 -4
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
527
|
-
|
|
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
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/executeQueryPlan.ts
CHANGED
|
@@ -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
|
-
|
|
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!,
|