@apollo/gateway 0.42.0 → 0.43.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
  });
@@ -6,11 +6,10 @@ import {
6
6
  import { GraphQLRequest, GraphQLExecutionResult, Logger } from 'apollo-server-types';
7
7
  import {
8
8
  composeAndValidate,
9
- buildSubgraphSchema,
10
9
  ServiceDefinition,
11
10
  compositionHasErrors,
12
11
  } from '@apollo/federation';
13
-
12
+ import { buildSubgraphSchema } from '@apollo/subgraph';
14
13
  import {
15
14
  executeQueryPlan,
16
15
  buildOperationContext,
@@ -111,7 +110,11 @@ export function getTestingSupergraphSdl(services: typeof fixtures = fixtures) {
111
110
  if (!compositionHasErrors(compositionResult)) {
112
111
  return compositionResult.supergraphSdl;
113
112
  }
114
- 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
+ );
115
118
  }
116
119
 
117
120
  export function wait(ms: number) {
@@ -1,5 +1,5 @@
1
1
  import { GraphQLSchemaModule } from 'apollo-graphql';
2
- import { buildFederatedSchema } from '@apollo/federation';
2
+ import { buildSubgraphSchema } from '@apollo/subgraph';
3
3
  import { ApolloServer } from 'apollo-server';
4
4
  import fetch from 'node-fetch';
5
5
  import { ApolloGateway } from '../..';
@@ -7,7 +7,7 @@ import { fixtures } from 'apollo-federation-integration-testsuite';
7
7
  import { ApolloServerPluginInlineTrace } from 'apollo-server-core';
8
8
 
9
9
  async function startFederatedServer(modules: GraphQLSchemaModule[]) {
10
- const schema = buildFederatedSchema(modules);
10
+ const schema = buildSubgraphSchema(modules);
11
11
  const server = new ApolloServer({
12
12
  schema,
13
13
  // Manually installing the inline trace plugin means it doesn't log a message.
@@ -4,7 +4,7 @@ import {fixtures, spanSerializer} from 'apollo-federation-integration-testsuite'
4
4
  import {fetch} from '../../__mocks__/apollo-server-env';
5
5
  import {InMemorySpanExporter, SimpleSpanProcessor} from '@opentelemetry/tracing'
6
6
  import {NodeTracerProvider} from '@opentelemetry/node';
7
- import { buildSubgraphSchema } from '@apollo/federation';
7
+ import { buildSubgraphSchema } from '@apollo/subgraph';
8
8
 
9
9
  expect.addSnapshotSerializer(spanSerializer);
10
10
 
@@ -1,6 +1,6 @@
1
1
  import gql from 'graphql-tag';
2
2
  import { ApolloServerBase as ApolloServer } from 'apollo-server-core';
3
- import { buildSubgraphSchema } from '@apollo/federation';
3
+ import { buildSubgraphSchema } from '@apollo/subgraph';
4
4
 
5
5
  import { LocalGraphQLDataSource } from '../../datasources/LocalGraphQLDataSource';
6
6
  import { ApolloGateway } from '../../';
@@ -2,7 +2,7 @@ import { gunzipSync } from 'zlib';
2
2
  import nock from 'nock';
3
3
  import { GraphQLSchemaModule } from 'apollo-graphql';
4
4
  import gql from 'graphql-tag';
5
- import { buildSubgraphSchema } from '@apollo/federation';
5
+ import { buildSubgraphSchema } from '@apollo/subgraph';
6
6
  import { ApolloServer } from 'apollo-server';
7
7
  import { ApolloServerPluginUsageReporting } from 'apollo-server-core';
8
8
  import { execute, toPromise } from 'apollo-link';
@@ -1,6 +1,6 @@
1
1
  import { execute } from '../execution-utils';
2
2
  import { ApolloServerBase as ApolloServer } from 'apollo-server-core';
3
- import { buildSubgraphSchema } from '@apollo/federation';
3
+ import { buildSubgraphSchema } from '@apollo/subgraph';
4
4
  import { LocalGraphQLDataSource } from '../../datasources/LocalGraphQLDataSource';
5
5
  import { ApolloGateway } from '../../';
6
6
  import { fixtures } from 'apollo-federation-integration-testsuite';
@@ -1,5 +1,5 @@
1
1
  import { LocalGraphQLDataSource } from '../LocalGraphQLDataSource';
2
- import { buildSubgraphSchema } from '@apollo/federation';
2
+ import { buildSubgraphSchema } from '@apollo/subgraph';
3
3
  import gql from 'graphql-tag';
4
4
  import { GraphQLResolverMap } from 'apollo-graphql';
5
5
  import { GraphQLRequestContext } from 'apollo-server-types';
@@ -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,
@@ -308,17 +338,17 @@ async function executeFetch<TContext>(
308
338
  const representations: ResultMap[] = [];
309
339
  const representationToEntity: number[] = [];
310
340
 
311
- entities.forEach((entity, index) => {
312
- const representation = executeSelectionSet(
313
- context.operationContext,
314
- entity,
315
- requires,
316
- );
317
- if (representation && representation[TypeNameMetaFieldDef.name]) {
318
- representations.push(representation);
319
- representationToEntity.push(index);
320
- }
321
- });
341
+ entities.forEach((entity, index) => {
342
+ const representation = executeSelectionSet(
343
+ context.operationContext,
344
+ entity,
345
+ requires,
346
+ );
347
+ if (representation && representation[TypeNameMetaFieldDef.name]) {
348
+ representations.push(representation);
349
+ representationToEntity.push(index);
350
+ }
351
+ });
322
352
 
323
353
  // If there are no representations, that means the type conditions in
324
354
  // the requires don't match any entities.
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { deprecate } from 'util';
1
2
  import { GraphQLService, Unsubscriber } from 'apollo-server-core';
2
3
  import {
3
4
  GraphQLExecutionResult,
@@ -117,7 +118,10 @@ export function getDefaultFetcher() {
117
118
  * TODO(trevor:cloudconfig): Stop exporting this
118
119
  * @deprecated This will be removed in a future version of @apollo/gateway
119
120
  */
120
- export const getDefaultGcsFetcher = getDefaultFetcher;
121
+ export const getDefaultGcsFetcher = deprecate(
122
+ getDefaultFetcher,
123
+ `'getDefaultGcsFetcher' is deprecated. Use 'getDefaultFetcher' instead.`,
124
+ );
121
125
  /**
122
126
  * TODO(trevor:cloudconfig): Stop exporting this
123
127
  * @deprecated This will be removed in a future version of @apollo/gateway
@@ -1270,6 +1274,11 @@ export class ApolloGateway implements GraphQLService {
1270
1274
  }
1271
1275
  }
1272
1276
 
1277
+ ApolloGateway.prototype.onSchemaChange = deprecate(
1278
+ ApolloGateway.prototype.onSchemaChange,
1279
+ `'ApolloGateway.prototype.onSchemaChange' is deprecated. Use 'ApolloGateway.prototype.onSchemaLoadOrUpdate' instead.`,
1280
+ );
1281
+
1273
1282
  function approximateObjectSize<T>(obj: T): number {
1274
1283
  return Buffer.byteLength(JSON.stringify(obj), 'utf8');
1275
1284
  }
@@ -8,12 +8,8 @@ import {
8
8
  Kind,
9
9
  ListTypeNode,
10
10
  NamedTypeNode,
11
- OperationDefinitionNode,
12
- parse,
13
- SelectionNode,
14
11
  TypeNode,
15
12
  } from 'graphql';
16
- import { assert } from './assert';
17
13
 
18
14
  export function getResponseName(node: FieldNode): string {
19
15
  return node.alias ? node.alias.value : node.name.value;
@@ -41,21 +37,3 @@ export function astFromType(type: GraphQLType): TypeNode {
41
37
  };
42
38
  }
43
39
  }
44
-
45
- /**
46
- * For lack of a "home of federation utilities", this function is copy/pasted
47
- * verbatim across the federation, gateway, and query-planner packages. Any changes
48
- * made here should be reflected in the other two locations as well.
49
- *
50
- * @param source A string representing a FieldSet
51
- * @returns A parsed FieldSet
52
- */
53
- export function parseSelections(source: string): ReadonlyArray<SelectionNode> {
54
- const parsed = parse(`{${source}}`);
55
- assert(
56
- parsed.definitions.length === 1,
57
- `Invalid FieldSet provided: '${source}'. FieldSets may not contain operations within them.`,
58
- );
59
- return (parsed.definitions[0] as OperationDefinitionNode).selectionSet
60
- .selections;
61
- }