@apollo/gateway 2.0.0-alpha.4 → 2.0.0-preview.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.
Files changed (47) hide show
  1. package/README.md +7 -5
  2. package/dist/__generated__/graphqlTypes.d.ts +3 -0
  3. package/dist/__generated__/graphqlTypes.d.ts.map +1 -1
  4. package/dist/__generated__/graphqlTypes.js.map +1 -1
  5. package/dist/executeQueryPlan.d.ts.map +1 -1
  6. package/dist/executeQueryPlan.js +4 -3
  7. package/dist/executeQueryPlan.js.map +1 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +9 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/schema-helper/index.js +5 -1
  13. package/dist/schema-helper/index.js.map +1 -1
  14. package/dist/supergraphManagers/IntrospectAndCompose/index.js.map +1 -1
  15. package/dist/supergraphManagers/LegacyFetcher/index.js +1 -1
  16. package/dist/supergraphManagers/LegacyFetcher/index.js.map +1 -1
  17. package/dist/supergraphManagers/LocalCompose/index.js +1 -1
  18. package/dist/supergraphManagers/LocalCompose/index.js.map +1 -1
  19. package/dist/supergraphManagers/UplinkFetcher/index.d.ts +1 -0
  20. package/dist/supergraphManagers/UplinkFetcher/index.d.ts.map +1 -1
  21. package/dist/supergraphManagers/UplinkFetcher/index.js +2 -0
  22. package/dist/supergraphManagers/UplinkFetcher/index.js.map +1 -1
  23. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.d.ts +5 -1
  24. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.d.ts.map +1 -1
  25. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.js +29 -31
  26. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.js.map +1 -1
  27. package/dist/supergraphManagers/index.d.ts +1 -0
  28. package/dist/supergraphManagers/index.d.ts.map +1 -1
  29. package/dist/supergraphManagers/index.js +3 -1
  30. package/dist/supergraphManagers/index.js.map +1 -1
  31. package/package.json +8 -7
  32. package/src/__generated__/graphqlTypes.ts +10 -1
  33. package/src/__tests__/build-query-plan.feature +84 -16
  34. package/src/__tests__/executeQueryPlan.test.ts +256 -1
  35. package/src/__tests__/gateway/lifecycle-hooks.test.ts +8 -2
  36. package/src/__tests__/gateway/supergraphSdl.test.ts +5 -3
  37. package/src/__tests__/integration/abstract-types.test.ts +6 -7
  38. package/src/__tests__/integration/nockMocks.ts +3 -2
  39. package/src/__tests__/integration/value-types.test.ts +4 -4
  40. package/src/executeQueryPlan.ts +4 -0
  41. package/src/index.ts +4 -1
  42. package/src/supergraphManagers/LegacyFetcher/index.ts +1 -1
  43. package/src/supergraphManagers/LocalCompose/index.ts +1 -1
  44. package/src/supergraphManagers/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts +82 -28
  45. package/src/supergraphManagers/UplinkFetcher/index.ts +2 -0
  46. package/src/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts +31 -26
  47. package/src/supergraphManagers/index.ts +1 -0
@@ -19,7 +19,7 @@ import { QueryPlan, QueryPlanner } from '@apollo/query-planner';
19
19
  import { ApolloGateway } from '..';
20
20
  import { ApolloServerBase as ApolloServer } from 'apollo-server-core';
21
21
  import { getFederatedTestingSchema } from './execution-utils';
22
- import { Schema, Operation, parseOperation, buildSchemaFromAST } from '@apollo/federation-internals';
22
+ import { Schema, Operation, parseOperation, buildSchemaFromAST, arrayEquals } from '@apollo/federation-internals';
23
23
  import { addResolversToSchema, GraphQLResolverMap } from '../schema-helper';
24
24
 
25
25
  expect.addSnapshotSerializer(astSerializer);
@@ -2736,4 +2736,259 @@ describe('executeQueryPlan', () => {
2736
2736
  expect(response.errors?.map((e) => e.message)).toStrictEqual(['String cannot represent value: ["invalid"]']);
2737
2737
  });
2738
2738
  });
2739
+
2740
+ describe('@key', () => {
2741
+ test('Works on a list of scalar', async () => {
2742
+ const s1_data = [
2743
+ { id: [0, 1], f1: "foo" },
2744
+ { id: [2, 3], f1: "bar" },
2745
+ { id: [4, 5], f1: "baz" },
2746
+ ];
2747
+
2748
+ const s1 = {
2749
+ name: 'S1',
2750
+ typeDefs: gql`
2751
+ type T1 @key(fields: "id") {
2752
+ id: [Int]
2753
+ f1: String
2754
+ }
2755
+ `,
2756
+ resolvers: {
2757
+ T1: {
2758
+ __resolveReference(ref: { id: number[] }) {
2759
+ return s1_data.find((e) => arrayEquals(e.id, ref.id));
2760
+ },
2761
+ },
2762
+ }
2763
+ }
2764
+
2765
+ const s2 = {
2766
+ name: 'S2',
2767
+ typeDefs: gql`
2768
+ type Query {
2769
+ getT1s: [T1]
2770
+ }
2771
+
2772
+ type T1 {
2773
+ id: [Int]
2774
+ }
2775
+ `,
2776
+ resolvers: {
2777
+ Query: {
2778
+ getT1s() {
2779
+ return [{id: [2, 3]}, {id: [4, 5]}];
2780
+ },
2781
+ },
2782
+ }
2783
+ }
2784
+
2785
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
2786
+
2787
+ const operation = parseOp(`
2788
+ query {
2789
+ getT1s {
2790
+ id
2791
+ f1
2792
+ }
2793
+ }
2794
+ `, schema);
2795
+ const queryPlan = buildPlan(operation, queryPlanner);
2796
+ expect(queryPlan).toMatchInlineSnapshot(`
2797
+ QueryPlan {
2798
+ Sequence {
2799
+ Fetch(service: "S2") {
2800
+ {
2801
+ getT1s {
2802
+ __typename
2803
+ id
2804
+ }
2805
+ }
2806
+ },
2807
+ Flatten(path: "getT1s.@") {
2808
+ Fetch(service: "S1") {
2809
+ {
2810
+ ... on T1 {
2811
+ __typename
2812
+ id
2813
+ }
2814
+ } =>
2815
+ {
2816
+ ... on T1 {
2817
+ f1
2818
+ }
2819
+ }
2820
+ },
2821
+ },
2822
+ },
2823
+ }
2824
+ `);
2825
+
2826
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2827
+ // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response.
2828
+ expect(response.data).toMatchInlineSnapshot(`
2829
+ Object {
2830
+ "getT1s": Array [
2831
+ Object {
2832
+ "f1": "bar",
2833
+ "id": Array [
2834
+ 2,
2835
+ 3,
2836
+ ],
2837
+ },
2838
+ Object {
2839
+ "f1": "baz",
2840
+ "id": Array [
2841
+ 4,
2842
+ 5,
2843
+ ],
2844
+ },
2845
+ ],
2846
+ }
2847
+ `);
2848
+ });
2849
+
2850
+ test('Works on a list of objects', async () => {
2851
+ const s1_data = [
2852
+ { o: [{a: 0, b: "b0", c: "zero"}, {a: 1, b: "b1", c: "one"}], f1: "foo" },
2853
+ { o: [{a: 2, b: "b2", c: "two"}], f1: "bar" },
2854
+ { o: [{a: 3, b: "b3", c: "three"}, {a: 4, b: "b4", c: "four"}], f1: "baz" },
2855
+ ];
2856
+
2857
+ const s1 = {
2858
+ name: 'S1',
2859
+ typeDefs: gql`
2860
+ type T1 @key(fields: "o { a c }") {
2861
+ o: [O]
2862
+ f1: String
2863
+ }
2864
+
2865
+ type O {
2866
+ a: Int
2867
+ b: String
2868
+ c: String
2869
+ }
2870
+ `,
2871
+ resolvers: {
2872
+ T1: {
2873
+ __resolveReference(ref: { o: {a : number, c: string}[] }) {
2874
+ return s1_data.find((e) => arrayEquals(e.o, ref.o, (x, y) => x.a === y.a && x.c === y.c));
2875
+ },
2876
+ },
2877
+ }
2878
+ }
2879
+
2880
+ const s2 = {
2881
+ name: 'S2',
2882
+ typeDefs: gql`
2883
+ type Query {
2884
+ getT1s: [T1]
2885
+ }
2886
+
2887
+ type T1 {
2888
+ o: [O]
2889
+ }
2890
+
2891
+ type O {
2892
+ a: Int
2893
+ b: String
2894
+ c: String
2895
+ }
2896
+ `,
2897
+ resolvers: {
2898
+ Query: {
2899
+ getT1s() {
2900
+ return [{o: [{a: 2, b: "b2", c: "two"}]}, {o: [{a: 3, b: "b3", c: "three"}, {a: 4, b: "b4", c: "four"}]}];
2901
+ },
2902
+ },
2903
+ }
2904
+ }
2905
+
2906
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
2907
+
2908
+ const operation = parseOp(`
2909
+ query {
2910
+ getT1s {
2911
+ o {
2912
+ a
2913
+ b
2914
+ c
2915
+ }
2916
+ f1
2917
+ }
2918
+ }
2919
+ `, schema);
2920
+ const queryPlan = buildPlan(operation, queryPlanner);
2921
+ expect(queryPlan).toMatchInlineSnapshot(`
2922
+ QueryPlan {
2923
+ Sequence {
2924
+ Fetch(service: "S2") {
2925
+ {
2926
+ getT1s {
2927
+ __typename
2928
+ o {
2929
+ a
2930
+ c
2931
+ b
2932
+ }
2933
+ }
2934
+ }
2935
+ },
2936
+ Flatten(path: "getT1s.@") {
2937
+ Fetch(service: "S1") {
2938
+ {
2939
+ ... on T1 {
2940
+ __typename
2941
+ o {
2942
+ a
2943
+ c
2944
+ }
2945
+ }
2946
+ } =>
2947
+ {
2948
+ ... on T1 {
2949
+ f1
2950
+ }
2951
+ }
2952
+ },
2953
+ },
2954
+ },
2955
+ }
2956
+ `);
2957
+
2958
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
2959
+ // `null` should bubble up since `f2` is now non-nullable. But we should still get the `id: 0` response.
2960
+ expect(response.data).toMatchInlineSnapshot(`
2961
+ Object {
2962
+ "getT1s": Array [
2963
+ Object {
2964
+ "f1": "bar",
2965
+ "o": Array [
2966
+ Object {
2967
+ "a": 2,
2968
+ "b": "b2",
2969
+ "c": "two",
2970
+ },
2971
+ ],
2972
+ },
2973
+ Object {
2974
+ "f1": "baz",
2975
+ "o": Array [
2976
+ Object {
2977
+ "a": 3,
2978
+ "b": "b3",
2979
+ "c": "three",
2980
+ },
2981
+ Object {
2982
+ "a": 4,
2983
+ "b": "b4",
2984
+ "c": "four",
2985
+ },
2986
+ ],
2987
+ },
2988
+ ],
2989
+ }
2990
+ `);
2991
+ });
2992
+ });
2993
+
2739
2994
  });
@@ -18,6 +18,8 @@ import {
18
18
  } from 'apollo-federation-integration-testsuite';
19
19
  import { Logger } from 'apollo-server-types';
20
20
  import resolvable from '@josephg/resolvable';
21
+ import { createHash } from '../../utilities/createHash';
22
+ import { getTestingSupergraphSdl } from '../execution-utils';
21
23
 
22
24
  // The order of this was specified to preserve existing test coverage. Typically
23
25
  // we would just import and use the `fixtures` array.
@@ -135,13 +137,17 @@ describe('lifecycle hooks', () => {
135
137
 
136
138
  const [firstCall, secondCall] = mockDidUpdate.mock.calls;
137
139
 
138
- const expectedFirstId = 'd20cfb7a9c51179aa494ed9e98153f0042892bd225437af064bf1c1aa68eab86'
140
+ // Note that we've composing our usual test fixtures here
141
+ const expectedFirstId = createHash('sha256').update(getTestingSupergraphSdl()).digest('hex');
139
142
  expect(firstCall[0]!.compositionId).toEqual(expectedFirstId);
140
143
  // first call should have no second "previous" argument
141
144
  expect(firstCall[1]).toBeUndefined();
142
145
 
146
+ // Note that this assertion is a tad fragile in that every time we modify
147
+ // the supergraph (even just formatting differences), this ID will change
148
+ // and this test will have to updated.
143
149
  expect(secondCall[0]!.compositionId).toEqual(
144
- '7dad7ab8284165a86241b8973d71f0d6ac8cb142095c717dd23443522850c225',
150
+ '3ca7f295b11b070d1e1b56a698cbfabb70cb2b5912a4ff0ecae2fb91e8709838',
145
151
  );
146
152
  // second call should have previous info in the second arg
147
153
  expect(secondCall[1]!.compositionId).toEqual(expectedFirstId);
@@ -53,6 +53,8 @@ afterEach(async () => {
53
53
  }
54
54
  });
55
55
 
56
+ const testingFixturesDefaultCompositionId = createHash('sha256').update(getTestingSupergraphSdl()).digest('hex');
57
+
56
58
  describe('Using supergraphSdl static configuration', () => {
57
59
  it('successfully starts and serves requests to the proper services', async () => {
58
60
  const server = await getSupergraphSdlGatewayServer();
@@ -187,7 +189,7 @@ describe('Using supergraphSdl dynamic configuration', () => {
187
189
  const { state, compositionId } = gateway.__testing();
188
190
  expect(state.phase).toEqual('loaded');
189
191
  expect(compositionId).toEqual(
190
- 'd20cfb7a9c51179aa494ed9e98153f0042892bd225437af064bf1c1aa68eab86',
192
+ testingFixturesDefaultCompositionId,
191
193
  );
192
194
 
193
195
  await gateway.stop();
@@ -212,7 +214,7 @@ describe('Using supergraphSdl dynamic configuration', () => {
212
214
  const { state, compositionId } = gateway.__testing();
213
215
  expect(state.phase).toEqual('loaded');
214
216
  expect(compositionId).toEqual(
215
- 'd20cfb7a9c51179aa494ed9e98153f0042892bd225437af064bf1c1aa68eab86',
217
+ testingFixturesDefaultCompositionId,
216
218
  );
217
219
 
218
220
  await expect(healthCheckCallback!(supergraphSdl)).resolves.toBeUndefined();
@@ -294,7 +296,7 @@ describe('Using supergraphSdl dynamic configuration', () => {
294
296
  const { state, compositionId } = gateway.__testing();
295
297
  expect(state.phase).toEqual('loaded');
296
298
  expect(compositionId).toEqual(
297
- 'd20cfb7a9c51179aa494ed9e98153f0042892bd225437af064bf1c1aa68eab86',
299
+ testingFixturesDefaultCompositionId
298
300
  );
299
301
 
300
302
  await expect(healthCheckCallback!(supergraphSdl)).rejects.toThrowError(
@@ -1,7 +1,6 @@
1
- import gql from 'graphql-tag';
2
1
  import { execute } from '../execution-utils';
3
2
 
4
- import { astSerializer, queryPlanSerializer } from 'apollo-federation-integration-testsuite';
3
+ import { astSerializer, fed2gql as gql, queryPlanSerializer } from 'apollo-federation-integration-testsuite';
5
4
 
6
5
  expect.addSnapshotSerializer(astSerializer);
7
6
  expect.addSnapshotSerializer(queryPlanSerializer);
@@ -849,7 +848,7 @@ describe("doesn't result in duplicate fetches", () => {
849
848
  type User @key(fields: "id") {
850
849
  id: ID!
851
850
  name: String
852
- username: String
851
+ username: String @shareable
853
852
  }
854
853
  `,
855
854
  },
@@ -1085,7 +1084,7 @@ it("when including the same nested fields under different type conditions", asyn
1085
1084
  type User @key(fields: "id") {
1086
1085
  id: ID!
1087
1086
  name: String
1088
- username: String
1087
+ username: String @shareable
1089
1088
  }
1090
1089
  `,
1091
1090
  },
@@ -1293,7 +1292,7 @@ it('when including multiple nested fields to the same service under different ty
1293
1292
  type User @key(fields: "id") {
1294
1293
  id: ID!
1295
1294
  name: String
1296
- username: String
1295
+ username: String @shareable
1297
1296
  }
1298
1297
  `,
1299
1298
  },
@@ -1514,7 +1513,7 @@ it('when exploding types through multiple levels', async () => {
1514
1513
  type User @key(fields: "id") {
1515
1514
  id: ID!
1516
1515
  name: String
1517
- username: String
1516
+ username: String @shareable
1518
1517
  }
1519
1518
  `,
1520
1519
  },
@@ -1700,7 +1699,7 @@ it("when including the same nested fields under different type conditions that a
1700
1699
  type User @key(fields: "id") {
1701
1700
  id: ID!
1702
1701
  name: String
1703
- username: String
1702
+ username: String @shareable
1704
1703
  }
1705
1704
  `,
1706
1705
  },
@@ -121,9 +121,10 @@ export function mockSupergraphSdlRequestSuccessIfAfter(
121
121
  }
122
122
 
123
123
  export function mockSupergraphSdlRequestIfAfterUnchanged(
124
- ifAfter: string | null = null,
124
+ ifAfter: string | null = null,
125
+ url: string = mockCloudConfigUrl1,
125
126
  ) {
126
- return mockSupergraphSdlRequestIfAfter(ifAfter).reply(
127
+ return mockSupergraphSdlRequestIfAfter(ifAfter, url).reply(
127
128
  200,
128
129
  JSON.stringify({
129
130
  data: {
@@ -1,7 +1,7 @@
1
- import gql from 'graphql-tag';
2
1
  import { execute } from '../execution-utils';
3
2
  import {
4
3
  astSerializer,
4
+ fed2gql as gql,
5
5
  queryPlanSerializer,
6
6
  } from 'apollo-federation-integration-testsuite';
7
7
 
@@ -200,7 +200,7 @@ describe('value types', () => {
200
200
  valueType: ValueType
201
201
  }
202
202
 
203
- type ValueType {
203
+ type ValueType @shareable {
204
204
  id: ID!
205
205
  user: User! @provides(fields: "name")
206
206
  }
@@ -226,7 +226,7 @@ describe('value types', () => {
226
226
  otherValueType: ValueType
227
227
  }
228
228
 
229
- type ValueType {
229
+ type ValueType @shareable {
230
230
  id: ID!
231
231
  user: User! @provides(fields: "name")
232
232
  }
@@ -250,7 +250,7 @@ describe('value types', () => {
250
250
  typeDefs: gql`
251
251
  type User @key(fields: "id") {
252
252
  id: ID!
253
- name: String!
253
+ name: String! @shareable
254
254
  address: String!
255
255
  }
256
256
  `,
@@ -298,6 +298,7 @@ async function executeFetch<TContext>(
298
298
  context,
299
299
  fetch.operation,
300
300
  variables,
301
+ fetch.operationName
301
302
  );
302
303
 
303
304
  for (const entity of entities) {
@@ -333,6 +334,7 @@ async function executeFetch<TContext>(
333
334
  context,
334
335
  fetch.operation,
335
336
  {...variables, representations},
337
+ fetch.operationName
336
338
  );
337
339
 
338
340
  if (!dataReceivedFromService) {
@@ -374,6 +376,7 @@ async function executeFetch<TContext>(
374
376
  context: ExecutionContext<TContext>,
375
377
  source: string,
376
378
  variables: Record<string, any>,
379
+ operationName: string | undefined
377
380
  ): Promise<ResultMap | void | null> {
378
381
  // We declare this as 'any' because it is missing url and method, which
379
382
  // GraphQLRequest.http is supposed to have if it exists.
@@ -402,6 +405,7 @@ async function executeFetch<TContext>(
402
405
  request: {
403
406
  query: source,
404
407
  variables,
408
+ operationName,
405
409
  http,
406
410
  },
407
411
  incomingRequestContext: context.requestContext,
package/src/index.ts CHANGED
@@ -402,7 +402,7 @@ export class ApolloGateway implements GraphQLService {
402
402
  apiKey: this.apolloConfig!.key!,
403
403
  uplinkEndpoints,
404
404
  maxRetries:
405
- this.config.uplinkMaxRetries ?? uplinkEndpoints.length * 3,
405
+ this.config.uplinkMaxRetries ?? uplinkEndpoints.length * 3 - 1, // -1 for the initial request
406
406
  subgraphHealthCheck: this.config.serviceHealthCheck,
407
407
  fetcher: this.fetcher,
408
408
  logger: this.logger,
@@ -1127,3 +1127,6 @@ export {
1127
1127
  SupergraphSdlHook,
1128
1128
  SupergraphManager
1129
1129
  } from './config';
1130
+
1131
+ export { UplinkFetcherError } from "./supergraphManagers"
1132
+
@@ -219,7 +219,7 @@ export class LegacyFetcher implements SupergraphManager {
219
219
 
220
220
  private logUpdateFailure(e: any) {
221
221
  this.config.logger?.error(
222
- 'UplinkFetcher failed to update supergraph with the following error: ' +
222
+ 'LegacyFetcher failed to update supergraph with the following error: ' +
223
223
  (e.message ?? e),
224
224
  );
225
225
  }
@@ -72,7 +72,7 @@ export class LocalCompose implements SupergraphManager {
72
72
 
73
73
  private logUpdateFailure(e: any) {
74
74
  this.config.logger?.error(
75
- 'UplinkFetcher failed to update supergraph with the following error: ' +
75
+ 'LocalCompose failed to update supergraph with the following error: ' +
76
76
  (e.message ?? e),
77
77
  );
78
78
  }