@apollo/gateway 0.43.1 → 2.0.0-alpha.1

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +2 -4
  2. package/LICENSE +95 -0
  3. package/dist/executeQueryPlan.d.ts +2 -3
  4. package/dist/executeQueryPlan.d.ts.map +1 -1
  5. package/dist/executeQueryPlan.js +2 -27
  6. package/dist/executeQueryPlan.js.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +22 -41
  10. package/dist/index.js.map +1 -1
  11. package/package.json +7 -5
  12. package/src/__tests__/build-query-plan.feature +34 -24
  13. package/src/__tests__/buildQueryPlan.test.ts +76 -840
  14. package/src/__tests__/executeQueryPlan.test.ts +543 -537
  15. package/src/__tests__/execution-utils.ts +20 -26
  16. package/src/__tests__/gateway/lifecycle-hooks.test.ts +26 -4
  17. package/src/__tests__/gateway/reporting.test.ts +0 -14
  18. package/src/__tests__/integration/abstract-types.test.ts +40 -52
  19. package/src/__tests__/integration/boolean.test.ts +3 -1
  20. package/src/__tests__/integration/complex-key.test.ts +40 -55
  21. package/src/__tests__/integration/configuration.test.ts +5 -5
  22. package/src/__tests__/integration/custom-directives.test.ts +8 -2
  23. package/src/__tests__/integration/multiple-key.test.ts +10 -11
  24. package/src/__tests__/integration/requires.test.ts +2 -2
  25. package/src/__tests__/integration/scope.test.ts +19 -30
  26. package/src/__tests__/integration/value-types.test.ts +31 -31
  27. package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +217 -142
  28. package/src/__tests__/queryPlanCucumber.test.ts +4 -18
  29. package/src/core/__tests__/core.test.ts +1 -0
  30. package/src/executeQueryPlan.ts +1 -32
  31. package/src/index.ts +39 -80
  32. package/src/utilities/__tests__/cleanErrorOfInaccessibleElements.test.ts +4 -4
  33. package/LICENSE.md +0 -21
  34. package/dist/core/index.d.ts +0 -13
  35. package/dist/core/index.d.ts.map +0 -1
  36. package/dist/core/index.js +0 -43
  37. package/dist/core/index.js.map +0 -1
  38. package/src/__tests__/build-query-plan-fragmentization.feature +0 -282
  39. package/src/core/index.ts +0 -51
@@ -5,23 +5,22 @@ import {
5
5
  } from 'apollo-graphql';
6
6
  import { GraphQLRequest, GraphQLExecutionResult, Logger } from 'apollo-server-types';
7
7
  import {
8
- composeAndValidate,
9
8
  ServiceDefinition,
10
- compositionHasErrors,
11
9
  } from '@apollo/federation';
12
10
  import { buildSubgraphSchema } from '@apollo/subgraph';
13
11
  import {
14
12
  executeQueryPlan,
15
13
  buildOperationContext,
16
14
  } from '@apollo/gateway';
17
- import { buildComposedSchema, QueryPlanner, QueryPlan } from '@apollo/query-planner';
15
+ import { QueryPlan, QueryPlanner } from '@apollo/query-planner';
18
16
  import { LocalGraphQLDataSource } from '../datasources/LocalGraphQLDataSource';
19
17
  import { mergeDeep } from 'apollo-utilities';
20
18
 
21
19
  import { queryPlanSerializer, astSerializer } from 'apollo-federation-integration-testsuite';
22
20
  import gql from 'graphql-tag';
23
21
  import { fixtures } from 'apollo-federation-integration-testsuite';
24
- import { parse } from 'graphql';
22
+ import { composeServices } from '@apollo/composition';
23
+ import { buildSchema, operationFromDocument } from '@apollo/federation-internals';
25
24
 
26
25
  const prettyFormat = require('pretty-format');
27
26
 
@@ -56,13 +55,15 @@ export async function execute(
56
55
 
57
56
  const { schema, queryPlanner } = getFederatedTestingSchema(services);
58
57
 
58
+ const operationDocument = gql`${request.query}`;
59
+ const operation = operationFromDocument(schema, operationDocument);
60
+ const queryPlan = queryPlanner.buildQueryPlan(operation);
61
+
59
62
  const operationContext = buildOperationContext({
60
- schema,
61
- operationDocument: gql`${request.query}`,
63
+ schema: schema.toAPISchema().toGraphQLJSSchema(),
64
+ operationDocument,
62
65
  });
63
66
 
64
- const queryPlan = queryPlanner.buildQueryPlan(operationContext);
65
-
66
67
  const result = await executeQueryPlan(
67
68
  queryPlan,
68
69
  serviceMap,
@@ -85,36 +86,29 @@ export function buildLocalService(modules: GraphQLSchemaModule[]) {
85
86
  }
86
87
 
87
88
  export function getFederatedTestingSchema(services: ServiceDefinitionModule[] = fixtures) {
89
+ const compositionResult = composeServices(services);
90
+ if (compositionResult.errors) {
91
+ throw new GraphQLSchemaValidationError(compositionResult.errors);
92
+ }
93
+
94
+ const queryPlanner = new QueryPlanner(compositionResult.schema);
95
+ const schema = buildSchema(compositionResult.supergraphSdl);
96
+
88
97
  const serviceMap = Object.fromEntries(
89
98
  services.map((service) => [
90
99
  service.name,
91
100
  buildLocalService([service]),
92
101
  ]),
93
102
  );
94
-
95
- const compositionResult = composeAndValidate(services);
96
-
97
- if (compositionHasErrors(compositionResult)) {
98
- throw new GraphQLSchemaValidationError(compositionResult.errors);
99
- }
100
-
101
- const schema = buildComposedSchema(parse(compositionResult.supergraphSdl))
102
-
103
- const queryPlanner = new QueryPlanner(schema);
104
-
105
103
  return { serviceMap, schema, queryPlanner };
106
104
  }
107
105
 
108
106
  export function getTestingSupergraphSdl(services: typeof fixtures = fixtures) {
109
- const compositionResult = composeAndValidate(services);
110
- if (!compositionHasErrors(compositionResult)) {
107
+ const compositionResult = composeServices(services);
108
+ if (!compositionResult.errors) {
111
109
  return compositionResult.supergraphSdl;
112
110
  }
113
- throw new Error(
114
- `Testing fixtures don't compose properly!\n${compositionResult.errors.join(
115
- '\t\n',
116
- )}`,
117
- );
111
+ throw new Error(`Testing fixtures don't compose properly!\nCauses:\n${compositionResult.errors.join('\n\n')}`);
118
112
  }
119
113
 
120
114
  export function wait(ms: number) {
@@ -70,10 +70,32 @@ describe('lifecycle hooks', () => {
70
70
  it('calls experimental_didFailComposition with a bad config', async () => {
71
71
  const experimental_didFailComposition = jest.fn();
72
72
 
73
+ // Creating 2 subservices that clearly cannot composed.
74
+ const s1 = {
75
+ name: 'S1',
76
+ url: 'http://S1',
77
+ typeDefs: gql`
78
+ type T {
79
+ a: Int
80
+ }
81
+ `
82
+ };
83
+
84
+ const s2 = {
85
+ name: 'S2',
86
+ url: 'http://S2',
87
+ typeDefs: gql`
88
+ type T {
89
+ a: String
90
+ }
91
+ `
92
+ };
93
+
94
+
73
95
  const gateway = new ApolloGateway({
74
96
  async experimental_updateServiceDefinitions() {
75
97
  return {
76
- serviceDefinitions: [serviceDefinitions[0]],
98
+ serviceDefinitions: [s1, s2],
77
99
  compositionMetadata: {
78
100
  formatVersion: 1,
79
101
  id: 'abc',
@@ -88,12 +110,12 @@ describe('lifecycle hooks', () => {
88
110
  logger,
89
111
  });
90
112
 
91
- await expect(gateway.load()).rejects.toThrowError();
113
+ await expect(gateway.load()).rejects.toThrowError("A valid schema couldn't be composed");
92
114
 
93
115
  const callbackArgs = experimental_didFailComposition.mock.calls[0][0];
94
- expect(callbackArgs.serviceList).toHaveLength(1);
116
+ expect(callbackArgs.serviceList).toHaveLength(2);
95
117
  expect(callbackArgs.errors[0]).toMatchInlineSnapshot(
96
- `[GraphQLError: [product] Book -> \`Book\` is an extension type, but \`Book\` is not defined in any service]`,
118
+ `[GraphQLError: Field "T.a" has incompatible types accross subgraphs: it has type "Int" in subgraph "S1" but type "String" in subgraph "S2"]`,
97
119
  );
98
120
  expect(callbackArgs.compositionMetadata.id).toEqual('abc');
99
121
  expect(experimental_didFailComposition).toBeCalled();
@@ -444,13 +444,6 @@ describe('reporting', () => {
444
444
  "child": Array [
445
445
  Object {
446
446
  "child": Array [
447
- Object {
448
- "endTime": "45678",
449
- "parentType": "Book",
450
- "responseName": "isbn",
451
- "startTime": "34567",
452
- "type": "String!",
453
- },
454
447
  Object {
455
448
  "endTime": "45678",
456
449
  "parentType": "Book",
@@ -470,13 +463,6 @@ describe('reporting', () => {
470
463
  },
471
464
  Object {
472
465
  "child": Array [
473
- Object {
474
- "endTime": "45678",
475
- "parentType": "Book",
476
- "responseName": "isbn",
477
- "startTime": "34567",
478
- "type": "String!",
479
- },
480
466
  Object {
481
467
  "endTime": "45678",
482
468
  "parentType": "Book",
@@ -39,17 +39,15 @@ it('handles an abstract type from the base service', async () => {
39
39
  {
40
40
  product(upc: $upc) {
41
41
  __typename
42
+ upc
42
43
  ... on Book {
43
- upc
44
44
  __typename
45
45
  isbn
46
- price
47
46
  }
48
47
  ... on Furniture {
49
- upc
50
48
  name
51
- price
52
49
  }
50
+ price
53
51
  }
54
52
  }
55
53
  },
@@ -63,8 +61,6 @@ it('handles an abstract type from the base service', async () => {
63
61
  } =>
64
62
  {
65
63
  ... on Book {
66
- __typename
67
- isbn
68
64
  title
69
65
  year
70
66
  }
@@ -76,9 +72,9 @@ it('handles an abstract type from the base service', async () => {
76
72
  {
77
73
  ... on Book {
78
74
  __typename
79
- isbn
80
75
  title
81
76
  year
77
+ isbn
82
78
  }
83
79
  } =>
84
80
  {
@@ -368,38 +364,38 @@ it('fetches interfaces returned from other services', async () => {
368
364
  },
369
365
  Parallel {
370
366
  Flatten(path: "me.reviews.@.product") {
371
- Fetch(service: "product") {
367
+ Fetch(service: "books") {
372
368
  {
373
369
  ... on Book {
374
370
  __typename
375
371
  isbn
376
372
  }
377
- ... on Furniture {
378
- __typename
379
- upc
380
- }
381
373
  } =>
382
374
  {
383
375
  ... on Book {
384
- price
385
- }
386
- ... on Furniture {
387
- price
376
+ title
388
377
  }
389
378
  }
390
379
  },
391
380
  },
392
381
  Flatten(path: "me.reviews.@.product") {
393
- Fetch(service: "books") {
382
+ Fetch(service: "product") {
394
383
  {
395
384
  ... on Book {
396
385
  __typename
397
386
  isbn
398
387
  }
388
+ ... on Furniture {
389
+ __typename
390
+ upc
391
+ }
399
392
  } =>
400
393
  {
401
394
  ... on Book {
402
- title
395
+ price
396
+ }
397
+ ... on Furniture {
398
+ price
403
399
  }
404
400
  }
405
401
  },
@@ -515,8 +511,6 @@ it('fetches composite fields from a foreign type casted to an interface [@provid
515
511
  } =>
516
512
  {
517
513
  ... on Book {
518
- __typename
519
- isbn
520
514
  title
521
515
  year
522
516
  }
@@ -528,9 +522,9 @@ it('fetches composite fields from a foreign type casted to an interface [@provid
528
522
  {
529
523
  ... on Book {
530
524
  __typename
531
- isbn
532
525
  title
533
526
  year
527
+ isbn
534
528
  }
535
529
  } =>
536
530
  {
@@ -943,30 +937,24 @@ describe("doesn't result in duplicate fetches", () => {
943
937
  {
944
938
  topProducts {
945
939
  __typename
940
+ name
941
+ price
946
942
  ... on Book {
947
- name
948
- price
949
943
  __typename
950
944
  isbn
951
945
  }
946
+ ... on TV {
947
+ __typename
948
+ id
949
+ }
952
950
  ... on Computer {
953
- name
954
- price
955
951
  __typename
956
952
  id
957
953
  }
958
954
  ... on Furniture {
959
- name
960
- price
961
955
  __typename
962
956
  sku
963
957
  }
964
- ... on TV {
965
- name
966
- price
967
- __typename
968
- id
969
- }
970
958
  }
971
959
  }
972
960
  },
@@ -977,6 +965,10 @@ describe("doesn't result in duplicate fetches", () => {
977
965
  __typename
978
966
  isbn
979
967
  }
968
+ ... on TV {
969
+ __typename
970
+ id
971
+ }
980
972
  ... on Computer {
981
973
  __typename
982
974
  id
@@ -985,10 +977,6 @@ describe("doesn't result in duplicate fetches", () => {
985
977
  __typename
986
978
  sku
987
979
  }
988
- ... on TV {
989
- __typename
990
- id
991
- }
992
980
  } =>
993
981
  {
994
982
  ... on Book {
@@ -1002,7 +990,7 @@ describe("doesn't result in duplicate fetches", () => {
1002
990
  id
1003
991
  }
1004
992
  }
1005
- ... on Computer {
993
+ ... on TV {
1006
994
  reviews {
1007
995
  author {
1008
996
  __typename
@@ -1013,7 +1001,7 @@ describe("doesn't result in duplicate fetches", () => {
1013
1001
  id
1014
1002
  }
1015
1003
  }
1016
- ... on Furniture {
1004
+ ... on Computer {
1017
1005
  reviews {
1018
1006
  author {
1019
1007
  __typename
@@ -1024,7 +1012,7 @@ describe("doesn't result in duplicate fetches", () => {
1024
1012
  id
1025
1013
  }
1026
1014
  }
1027
- ... on TV {
1015
+ ... on Furniture {
1028
1016
  reviews {
1029
1017
  author {
1030
1018
  __typename
@@ -1186,16 +1174,16 @@ it("when including the same nested fields under different type conditions", asyn
1186
1174
  topProducts {
1187
1175
  __typename
1188
1176
  ... on Book {
1189
- name
1190
- price
1191
1177
  __typename
1192
1178
  isbn
1193
- }
1194
- ... on TV {
1195
1179
  name
1196
1180
  price
1181
+ }
1182
+ ... on TV {
1197
1183
  __typename
1198
1184
  id
1185
+ name
1186
+ price
1199
1187
  }
1200
1188
  }
1201
1189
  }
@@ -1395,16 +1383,16 @@ it('when including multiple nested fields to the same service under different ty
1395
1383
  topProducts {
1396
1384
  __typename
1397
1385
  ... on Book {
1398
- name
1399
- price
1400
1386
  __typename
1401
1387
  isbn
1402
- }
1403
- ... on TV {
1404
1388
  name
1405
1389
  price
1390
+ }
1391
+ ... on TV {
1406
1392
  __typename
1407
1393
  id
1394
+ name
1395
+ price
1408
1396
  }
1409
1397
  }
1410
1398
  }
@@ -1788,16 +1776,16 @@ it("when including the same nested fields under different type conditions that a
1788
1776
  topProducts {
1789
1777
  __typename
1790
1778
  ... on Book {
1791
- name
1792
- price
1793
1779
  __typename
1794
1780
  isbn
1795
- }
1796
- ... on TV {
1797
1781
  name
1798
1782
  price
1783
+ }
1784
+ ... on TV {
1799
1785
  __typename
1800
1786
  id
1787
+ name
1788
+ price
1801
1789
  }
1802
1790
  }
1803
1791
  }
@@ -13,7 +13,9 @@ describe('@skip', () => {
13
13
  topReviews {
14
14
  body
15
15
  author @skip(if: true) {
16
- name
16
+ name {
17
+ first
18
+ }
17
19
  }
18
20
  }
19
21
  }
@@ -123,6 +123,46 @@ it('works fetches data correctly with complex / nested @key fields', async () =>
123
123
  [userService, reviewService],
124
124
  );
125
125
 
126
+ expect(queryPlan).toMatchInlineSnapshot(`
127
+ QueryPlan {
128
+ Sequence {
129
+ Fetch(service: "review") {
130
+ {
131
+ reviews {
132
+ author {
133
+ __typename
134
+ id
135
+ organization {
136
+ id
137
+ }
138
+ }
139
+ }
140
+ }
141
+ },
142
+ Flatten(path: "reviews.@.author") {
143
+ Fetch(service: "user") {
144
+ {
145
+ ... on User {
146
+ __typename
147
+ id
148
+ organization {
149
+ id
150
+ }
151
+ }
152
+ } =>
153
+ {
154
+ ... on User {
155
+ name
156
+ organization {
157
+ name
158
+ }
159
+ }
160
+ }
161
+ },
162
+ },
163
+ },
164
+ }
165
+ `);
126
166
  expect(data).toEqual({
127
167
  reviews: [
128
168
  {
@@ -159,59 +199,4 @@ it('works fetches data correctly with complex / nested @key fields', async () =>
159
199
  },
160
200
  ],
161
201
  });
162
- expect(queryPlan).toMatchInlineSnapshot(`
163
- QueryPlan {
164
- Sequence {
165
- Fetch(service: "review") {
166
- {
167
- reviews {
168
- author {
169
- __typename
170
- id
171
- organization {
172
- id
173
- __typename
174
- }
175
- }
176
- }
177
- }
178
- },
179
- Parallel {
180
- Flatten(path: "reviews.@.author") {
181
- Fetch(service: "user") {
182
- {
183
- ... on User {
184
- __typename
185
- id
186
- organization {
187
- id
188
- }
189
- }
190
- } =>
191
- {
192
- ... on User {
193
- name
194
- }
195
- }
196
- },
197
- },
198
- Flatten(path: "reviews.@.author.organization") {
199
- Fetch(service: "user") {
200
- {
201
- ... on Organization {
202
- __typename
203
- id
204
- }
205
- } =>
206
- {
207
- ... on Organization {
208
- name
209
- }
210
- }
211
- },
212
- },
213
- },
214
- },
215
- }
216
- `);
217
202
  });
@@ -182,11 +182,11 @@ describe('gateway startup errors', () => {
182
182
  err = e;
183
183
  }
184
184
 
185
- expect(err.message).toMatchInlineSnapshot(`
186
- "A valid schema couldn't be composed. The following composition errors were found:
187
- [accounts] Account -> A @key selects id, but Account.id could not be found
188
- [accounts] User -> A @key selects id, but User.id could not be found"
189
- `);
185
+ const expected =
186
+ "A valid schema couldn't be composed. The following composition errors were found:\n"
187
+ + ' [accounts] On type "User", for @key(fields: "id"): Cannot query field "id" on type "User" (the field should be either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).\n'
188
+ + ' [accounts] On type "Account", for @key(fields: "id"): Cannot query field "id" on type "Account" (the field should be either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).'
189
+ expect(err.message).toBe(expected);
190
190
  });
191
191
  });
192
192
 
@@ -97,7 +97,10 @@ describe('custom executable directives', () => {
97
97
  `);
98
98
  });
99
99
 
100
- it("returns validation errors when directives aren't present across all services", async () => {
100
+ // With relaxed composition, instead of erroring out if a directive is not declared everywhere, we compose but don't
101
+ // include the directive in the supergraph and generate a hint. So the following test will complain that @stream
102
+ // is unknown in the query. Not that the hints tests do test we properly raise an hint in that case.
103
+ it.skip("returns validation errors when directives aren't present across all services", async () => {
101
104
  const invalidService = {
102
105
  name: 'invalidService',
103
106
  typeDefs: gql`
@@ -129,7 +132,10 @@ describe('custom executable directives', () => {
129
132
  `);
130
133
  });
131
134
 
132
- it("returns validation errors when directives aren't identical across all services", async () => {
135
+ // Same as previous: we don't of error out on inconsistent execution directives. Here, we instead look at the intersection of locations
136
+ // defined, and as that is empty, we don't include the directive in the supergraph (and raise a hint).
137
+ // So the following test will complain that @stream is unknown in the query. Not that the hints tests do test we properly raise an hint in that case.
138
+ it.skip("returns validation errors when directives aren't identical across all services", async () => {
133
139
  const invalidService = {
134
140
  name: 'invalid',
135
141
  typeDefs: gql`
@@ -192,9 +192,8 @@ it('fetches data correctly with multiple @key fields', async () => {
192
192
  } =>
193
193
  {
194
194
  ... on User {
195
- name
196
- __typename
197
195
  ssn
196
+ name
198
197
  }
199
198
  }
200
199
  },
@@ -283,40 +282,40 @@ it('fetches keys as needed to reduce round trip queries', async () => {
283
282
  {
284
283
  users {
285
284
  __typename
286
- ssn
287
285
  id
286
+ ssn
288
287
  }
289
288
  }
290
289
  },
291
290
  Parallel {
292
291
  Flatten(path: "users.@") {
293
- Fetch(service: "actuary") {
292
+ Fetch(service: "reviews") {
294
293
  {
295
294
  ... on User {
296
295
  __typename
297
- ssn
296
+ id
298
297
  }
299
298
  } =>
300
299
  {
301
300
  ... on User {
302
- risk
301
+ reviews {
302
+ body
303
+ }
303
304
  }
304
305
  }
305
306
  },
306
307
  },
307
308
  Flatten(path: "users.@") {
308
- Fetch(service: "reviews") {
309
+ Fetch(service: "actuary") {
309
310
  {
310
311
  ... on User {
311
312
  __typename
312
- id
313
+ ssn
313
314
  }
314
315
  } =>
315
316
  {
316
317
  ... on User {
317
- reviews {
318
- body
319
- }
318
+ risk
320
319
  }
321
320
  }
322
321
  },
@@ -140,8 +140,8 @@ it('collapses nested requires', async () => {
140
140
  id
141
141
  preferences {
142
142
  favorites {
143
- color
144
143
  animal
144
+ color
145
145
  }
146
146
  }
147
147
  }
@@ -155,8 +155,8 @@ it('collapses nested requires', async () => {
155
155
  id
156
156
  preferences {
157
157
  favorites {
158
- color
159
158
  animal
159
+ color
160
160
  }
161
161
  }
162
162
  }