@apollo/gateway 2.0.0-alpha.0 → 2.0.0-alpha.4

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 (154) hide show
  1. package/README.md +1 -1
  2. package/dist/__generated__/graphqlTypes.d.ts +13 -11
  3. package/dist/__generated__/graphqlTypes.d.ts.map +1 -1
  4. package/dist/__generated__/graphqlTypes.js.map +1 -1
  5. package/dist/config.d.ts +45 -24
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +30 -31
  8. package/dist/config.js.map +1 -1
  9. package/dist/datasources/LocalGraphQLDataSource.js.map +1 -1
  10. package/dist/datasources/RemoteGraphQLDataSource.d.ts.map +1 -1
  11. package/dist/datasources/RemoteGraphQLDataSource.js +4 -1
  12. package/dist/datasources/RemoteGraphQLDataSource.js.map +1 -1
  13. package/dist/datasources/types.d.ts +1 -1
  14. package/dist/datasources/types.d.ts.map +1 -1
  15. package/dist/executeQueryPlan.d.ts.map +1 -1
  16. package/dist/executeQueryPlan.js +6 -6
  17. package/dist/executeQueryPlan.js.map +1 -1
  18. package/dist/index.d.ts +36 -23
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +197 -297
  21. package/dist/index.js.map +1 -1
  22. package/dist/operationContext.js +0 -1
  23. package/dist/operationContext.js.map +1 -1
  24. package/dist/schema-helper/addResolversToSchema.d.ts +4 -0
  25. package/dist/schema-helper/addResolversToSchema.d.ts.map +1 -0
  26. package/dist/schema-helper/addResolversToSchema.js +62 -0
  27. package/dist/schema-helper/addResolversToSchema.js.map +1 -0
  28. package/dist/schema-helper/error.d.ts +6 -0
  29. package/dist/schema-helper/error.d.ts.map +1 -0
  30. package/dist/schema-helper/error.js +14 -0
  31. package/dist/schema-helper/error.js.map +1 -0
  32. package/dist/schema-helper/index.d.ts +4 -0
  33. package/dist/schema-helper/index.d.ts.map +1 -0
  34. package/dist/schema-helper/index.js +16 -0
  35. package/dist/schema-helper/index.js.map +1 -0
  36. package/dist/schema-helper/resolverMap.d.ts +16 -0
  37. package/dist/schema-helper/resolverMap.d.ts.map +1 -0
  38. package/dist/schema-helper/resolverMap.js +3 -0
  39. package/dist/schema-helper/resolverMap.js.map +1 -0
  40. package/dist/supergraphManagers/IntrospectAndCompose/index.d.ts +31 -0
  41. package/dist/supergraphManagers/IntrospectAndCompose/index.d.ts.map +1 -0
  42. package/dist/supergraphManagers/IntrospectAndCompose/index.js +112 -0
  43. package/dist/supergraphManagers/IntrospectAndCompose/index.js.map +1 -0
  44. package/dist/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.d.ts +12 -0
  45. package/dist/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.d.ts.map +1 -0
  46. package/dist/{loadServicesFromRemoteEndpoint.js → supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.js} +6 -6
  47. package/dist/supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.js.map +1 -0
  48. package/dist/supergraphManagers/LegacyFetcher/index.d.ts +33 -0
  49. package/dist/supergraphManagers/LegacyFetcher/index.d.ts.map +1 -0
  50. package/dist/supergraphManagers/LegacyFetcher/index.js +149 -0
  51. package/dist/supergraphManagers/LegacyFetcher/index.js.map +1 -0
  52. package/dist/supergraphManagers/LocalCompose/index.d.ts +19 -0
  53. package/dist/supergraphManagers/LocalCompose/index.d.ts.map +1 -0
  54. package/dist/supergraphManagers/LocalCompose/index.js +55 -0
  55. package/dist/supergraphManagers/LocalCompose/index.js.map +1 -0
  56. package/dist/supergraphManagers/UplinkFetcher/index.d.ts +32 -0
  57. package/dist/supergraphManagers/UplinkFetcher/index.d.ts.map +1 -0
  58. package/dist/supergraphManagers/UplinkFetcher/index.js +96 -0
  59. package/dist/supergraphManagers/UplinkFetcher/index.js.map +1 -0
  60. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.d.ts +21 -0
  61. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.d.ts.map +1 -0
  62. package/dist/{loadSupergraphSdlFromStorage.js → supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.js} +41 -10
  63. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.js.map +1 -0
  64. package/dist/supergraphManagers/UplinkFetcher/outOfBandReporter.d.ts +13 -0
  65. package/dist/supergraphManagers/UplinkFetcher/outOfBandReporter.d.ts.map +1 -0
  66. package/dist/supergraphManagers/UplinkFetcher/outOfBandReporter.js +85 -0
  67. package/dist/supergraphManagers/UplinkFetcher/outOfBandReporter.js.map +1 -0
  68. package/dist/supergraphManagers/index.d.ts +5 -0
  69. package/dist/supergraphManagers/index.d.ts.map +1 -0
  70. package/dist/supergraphManagers/index.js +12 -0
  71. package/dist/supergraphManagers/index.js.map +1 -0
  72. package/dist/utilities/array.js +1 -1
  73. package/dist/utilities/array.js.map +1 -1
  74. package/dist/utilities/createHash.d.ts +2 -0
  75. package/dist/utilities/createHash.d.ts.map +1 -0
  76. package/dist/utilities/createHash.js +15 -0
  77. package/dist/utilities/createHash.js.map +1 -0
  78. package/dist/utilities/isNodeLike.d.ts +3 -0
  79. package/dist/utilities/isNodeLike.d.ts.map +1 -0
  80. package/dist/utilities/isNodeLike.js +8 -0
  81. package/dist/utilities/isNodeLike.js.map +1 -0
  82. package/package.json +9 -9
  83. package/src/__generated__/graphqlTypes.ts +13 -11
  84. package/src/__mocks__/make-fetch-happen-fetcher.ts +3 -1
  85. package/src/__tests__/buildQueryPlan.test.ts +1 -1
  86. package/src/__tests__/executeQueryPlan.test.ts +1171 -77
  87. package/src/__tests__/execution-utils.ts +5 -7
  88. package/src/__tests__/gateway/buildService.test.ts +3 -3
  89. package/src/__tests__/gateway/endToEnd.test.ts +1 -1
  90. package/src/__tests__/gateway/executor.test.ts +3 -1
  91. package/src/__tests__/gateway/lifecycle-hooks.test.ts +59 -121
  92. package/src/__tests__/gateway/opentelemetry.test.ts +8 -3
  93. package/src/__tests__/gateway/queryPlanCache.test.ts +25 -9
  94. package/src/__tests__/gateway/reporting.test.ts +42 -13
  95. package/src/__tests__/gateway/supergraphSdl.test.ts +397 -0
  96. package/src/__tests__/integration/aliases.test.ts +9 -3
  97. package/src/__tests__/integration/configuration.test.ts +140 -21
  98. package/src/__tests__/integration/logger.test.ts +2 -2
  99. package/src/__tests__/integration/networkRequests.test.ts +126 -149
  100. package/src/__tests__/integration/nockMocks.ts +57 -16
  101. package/src/__tests__/nockAssertions.ts +20 -0
  102. package/src/config.ts +153 -77
  103. package/src/core/__tests__/core.test.ts +6 -6
  104. package/src/datasources/LocalGraphQLDataSource.ts +1 -1
  105. package/src/datasources/RemoteGraphQLDataSource.ts +8 -2
  106. package/src/datasources/__tests__/LocalGraphQLDataSource.test.ts +1 -1
  107. package/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +4 -4
  108. package/src/datasources/types.ts +1 -1
  109. package/src/executeQueryPlan.ts +18 -9
  110. package/src/index.ts +323 -481
  111. package/src/make-fetch-happen.d.ts +1 -1
  112. package/src/operationContext.ts +2 -2
  113. package/src/schema-helper/addResolversToSchema.ts +83 -0
  114. package/src/schema-helper/error.ts +11 -0
  115. package/src/schema-helper/index.ts +3 -0
  116. package/src/schema-helper/resolverMap.ts +23 -0
  117. package/src/supergraphManagers/IntrospectAndCompose/__tests__/IntrospectAndCompose.test.ts +370 -0
  118. package/src/{__tests__ → supergraphManagers/IntrospectAndCompose/__tests__}/loadServicesFromRemoteEndpoint.test.ts +7 -7
  119. package/src/supergraphManagers/IntrospectAndCompose/__tests__/tsconfig.json +8 -0
  120. package/src/supergraphManagers/IntrospectAndCompose/index.ts +160 -0
  121. package/src/{loadServicesFromRemoteEndpoint.ts → supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts} +7 -7
  122. package/src/supergraphManagers/LegacyFetcher/index.ts +226 -0
  123. package/src/supergraphManagers/LocalCompose/index.ts +79 -0
  124. package/src/supergraphManagers/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts +343 -0
  125. package/src/supergraphManagers/UplinkFetcher/__tests__/tsconfig.json +8 -0
  126. package/src/supergraphManagers/UplinkFetcher/index.ts +128 -0
  127. package/src/{loadSupergraphSdlFromStorage.ts → supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts} +63 -14
  128. package/src/supergraphManagers/UplinkFetcher/outOfBandReporter.ts +126 -0
  129. package/src/supergraphManagers/index.ts +4 -0
  130. package/src/utilities/__tests__/cleanErrorOfInaccessibleElements.test.ts +13 -10
  131. package/src/utilities/array.ts +1 -1
  132. package/src/utilities/createHash.ts +10 -0
  133. package/src/utilities/isNodeLike.ts +11 -0
  134. package/CHANGELOG.md +0 -452
  135. package/dist/legacyLoadServicesFromStorage.d.ts +0 -20
  136. package/dist/legacyLoadServicesFromStorage.d.ts.map +0 -1
  137. package/dist/legacyLoadServicesFromStorage.js +0 -62
  138. package/dist/legacyLoadServicesFromStorage.js.map +0 -1
  139. package/dist/loadServicesFromRemoteEndpoint.d.ts +0 -13
  140. package/dist/loadServicesFromRemoteEndpoint.d.ts.map +0 -1
  141. package/dist/loadServicesFromRemoteEndpoint.js.map +0 -1
  142. package/dist/loadSupergraphSdlFromStorage.d.ts +0 -12
  143. package/dist/loadSupergraphSdlFromStorage.d.ts.map +0 -1
  144. package/dist/loadSupergraphSdlFromStorage.js.map +0 -1
  145. package/dist/outOfBandReporter.d.ts +0 -15
  146. package/dist/outOfBandReporter.d.ts.map +0 -1
  147. package/dist/outOfBandReporter.js +0 -88
  148. package/dist/outOfBandReporter.js.map +0 -1
  149. package/src/__tests__/gateway/composedSdl.test.ts +0 -44
  150. package/src/__tests__/integration/legacyNetworkRequests.test.ts +0 -279
  151. package/src/__tests__/integration/legacyNockMocks.ts +0 -113
  152. package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +0 -664
  153. package/src/legacyLoadServicesFromStorage.ts +0 -170
  154. package/src/outOfBandReporter.ts +0 -128
@@ -0,0 +1,343 @@
1
+ import {
2
+ loadSupergraphSdlFromStorage,
3
+ loadSupergraphSdlFromUplinks
4
+ } from '../loadSupergraphSdlFromStorage';
5
+ import { getDefaultFetcher } from '../../..';
6
+ import {
7
+ graphRef,
8
+ apiKey,
9
+ mockCloudConfigUrl1,
10
+ mockCloudConfigUrl2,
11
+ mockOutOfBandReporterUrl,
12
+ mockSupergraphSdlRequest,
13
+ mockOutOfBandReportRequestSuccess,
14
+ mockSupergraphSdlRequestSuccess,
15
+ mockSupergraphSdlRequestIfAfterUnchanged,
16
+ mockSupergraphSdlRequestIfAfter
17
+ } from '../../../__tests__/integration/nockMocks';
18
+ import { getTestingSupergraphSdl } from "../../../__tests__/execution-utils";
19
+ import { nockAfterEach, nockBeforeEach } from '../../../__tests__/nockAssertions';
20
+
21
+ describe('loadSupergraphSdlFromStorage', () => {
22
+ beforeEach(nockBeforeEach);
23
+ afterEach(nockAfterEach);
24
+
25
+ it('fetches Supergraph SDL as expected', async () => {
26
+ mockSupergraphSdlRequestSuccess();
27
+ const fetcher = getDefaultFetcher();
28
+ const result = await loadSupergraphSdlFromStorage({
29
+ graphRef,
30
+ apiKey,
31
+ endpoint: mockCloudConfigUrl1,
32
+ errorReportingEndpoint: undefined,
33
+ fetcher,
34
+ compositionId: null,
35
+ });
36
+ expect(result).toMatchObject({
37
+ id: 'originalId-1234',
38
+ supergraphSdl: getTestingSupergraphSdl(),
39
+ });
40
+ });
41
+
42
+ it('Queries alternate Uplink URL if first one fails', async () => {
43
+ mockSupergraphSdlRequest('originalId-1234', mockCloudConfigUrl1).reply(500);
44
+ mockSupergraphSdlRequestIfAfter('originalId-1234', mockCloudConfigUrl2).reply(
45
+ 200,
46
+ JSON.stringify({
47
+ data: {
48
+ routerConfig: {
49
+ __typename: 'RouterConfigResult',
50
+ id: 'originalId-1234',
51
+ supergraphSdl: getTestingSupergraphSdl()
52
+ },
53
+ },
54
+ }),
55
+ );
56
+
57
+ const fetcher = getDefaultFetcher();
58
+ const result = await loadSupergraphSdlFromUplinks({
59
+ graphRef,
60
+ apiKey,
61
+ endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2],
62
+ errorReportingEndpoint: undefined,
63
+ fetcher,
64
+ compositionId: "originalId-1234",
65
+ maxRetries: 1
66
+ });
67
+
68
+ expect(result).toMatchObject({
69
+ id: 'originalId-1234',
70
+ supergraphSdl: getTestingSupergraphSdl(),
71
+ });
72
+ });
73
+
74
+ it('Throws error if all Uplink URLs fail', async () => {
75
+ mockSupergraphSdlRequest("originalId-1234", mockCloudConfigUrl1).reply(500);
76
+ mockSupergraphSdlRequestIfAfter("originalId-1234", mockCloudConfigUrl2).reply(500);
77
+
78
+ const fetcher = getDefaultFetcher();
79
+ await expect(
80
+ loadSupergraphSdlFromUplinks({
81
+ graphRef,
82
+ apiKey,
83
+ endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2],
84
+ errorReportingEndpoint: undefined,
85
+ fetcher,
86
+ compositionId: "originalId-1234",
87
+ maxRetries: 1
88
+ }),
89
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
90
+ `"An error occurred while fetching your schema from Apollo: 500 Internal Server Error"`,
91
+ );
92
+ })
93
+
94
+ describe('errors', () => {
95
+ it('throws on a malformed response', async () => {
96
+ mockSupergraphSdlRequest().reply(200, 'Invalid JSON');
97
+
98
+ const fetcher = getDefaultFetcher();
99
+ await expect(
100
+ loadSupergraphSdlFromStorage({
101
+ graphRef,
102
+ apiKey,
103
+ endpoint: mockCloudConfigUrl1,
104
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
105
+ fetcher,
106
+ compositionId: null,
107
+ }),
108
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
109
+ `"An error occurred while fetching your schema from Apollo: 200 invalid json response body at https://example1.cloud-config-url.com/cloudconfig/ reason: Unexpected token I in JSON at position 0"`,
110
+ );
111
+ });
112
+
113
+ it('throws errors from JSON on 400', async () => {
114
+ const message = 'Query syntax error';
115
+ mockSupergraphSdlRequest().reply(
116
+ 400,
117
+ JSON.stringify({
118
+ errors: [{ message }],
119
+ }),
120
+ );
121
+
122
+ const fetcher = getDefaultFetcher();
123
+ await expect(
124
+ loadSupergraphSdlFromStorage({
125
+ graphRef,
126
+ apiKey,
127
+ endpoint: mockCloudConfigUrl1,
128
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
129
+ fetcher,
130
+ compositionId: null,
131
+ }),
132
+ ).rejects.toThrowError(message);
133
+ });
134
+
135
+ it("throws on non-OK status codes when `errors` isn't present in a JSON response", async () => {
136
+ mockSupergraphSdlRequest().reply(500);
137
+ mockOutOfBandReportRequestSuccess();
138
+
139
+ const fetcher = getDefaultFetcher();
140
+ await expect(
141
+ loadSupergraphSdlFromStorage({
142
+ graphRef,
143
+ apiKey,
144
+ endpoint: mockCloudConfigUrl1,
145
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
146
+ fetcher,
147
+ compositionId: null,
148
+ }),
149
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
150
+ `"An error occurred while fetching your schema from Apollo: 500 Internal Server Error"`,
151
+ );
152
+ });
153
+
154
+ // if an additional request were made by the out of band reporter, nock would throw since it's unmocked
155
+ // and this test would fail
156
+ it("Out of band reporting doesn't submit reports when endpoint is not configured", async () => {
157
+ mockSupergraphSdlRequest().reply(400);
158
+
159
+ const fetcher = getDefaultFetcher();
160
+ await expect(
161
+ loadSupergraphSdlFromStorage({
162
+ graphRef,
163
+ apiKey,
164
+ endpoint: mockCloudConfigUrl1,
165
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
166
+ fetcher,
167
+ compositionId: null,
168
+ }),
169
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
170
+ `"An error occurred while fetching your schema from Apollo: 400 invalid json response body at https://example1.cloud-config-url.com/cloudconfig/ reason: Unexpected end of JSON input"`,
171
+ );
172
+ });
173
+
174
+ it('throws on 400 status response and does not submit an out of band error', async () => {
175
+
176
+ mockSupergraphSdlRequest().reply(400);
177
+
178
+ const fetcher = getDefaultFetcher();
179
+ await expect(
180
+ loadSupergraphSdlFromStorage({
181
+ graphRef,
182
+ apiKey,
183
+ endpoint: mockCloudConfigUrl1,
184
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
185
+ fetcher,
186
+ compositionId: null,
187
+ }),
188
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
189
+ `"An error occurred while fetching your schema from Apollo: 400 invalid json response body at https://example1.cloud-config-url.com/cloudconfig/ reason: Unexpected end of JSON input"`,
190
+ );
191
+ });
192
+
193
+ it('throws on 413 status response and successfully submits an out of band error', async () => {
194
+ mockSupergraphSdlRequest().reply(413);
195
+ mockOutOfBandReportRequestSuccess();
196
+
197
+ const fetcher = getDefaultFetcher();
198
+ await expect(
199
+ loadSupergraphSdlFromStorage({
200
+ graphRef,
201
+ apiKey,
202
+ endpoint: mockCloudConfigUrl1,
203
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
204
+ fetcher,
205
+ compositionId: null,
206
+ }),
207
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
208
+ `"An error occurred while fetching your schema from Apollo: 413 Payload Too Large"`,
209
+ );
210
+ });
211
+
212
+ it('throws on 422 status response and successfully submits an out of band error', async () => {
213
+ mockSupergraphSdlRequest().reply(422);
214
+ mockOutOfBandReportRequestSuccess();
215
+
216
+ const fetcher = getDefaultFetcher();
217
+ await expect(
218
+ loadSupergraphSdlFromStorage({
219
+ graphRef,
220
+ apiKey,
221
+ endpoint: mockCloudConfigUrl1,
222
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
223
+ fetcher,
224
+ compositionId: null,
225
+ }),
226
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
227
+ `"An error occurred while fetching your schema from Apollo: 422 Unprocessable Entity"`,
228
+ );
229
+ });
230
+
231
+ it('throws on 408 status response and successfully submits an out of band error', async () => {
232
+ mockSupergraphSdlRequest().reply(408);
233
+ mockOutOfBandReportRequestSuccess();
234
+
235
+ const fetcher = getDefaultFetcher();
236
+ await expect(
237
+ loadSupergraphSdlFromStorage({
238
+ graphRef,
239
+ apiKey,
240
+ endpoint: mockCloudConfigUrl1,
241
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
242
+ fetcher,
243
+ compositionId: null,
244
+ }),
245
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
246
+ `"An error occurred while fetching your schema from Apollo: 408 Request Timeout"`,
247
+ );
248
+ });
249
+ });
250
+
251
+ it('throws on 504 status response and successfully submits an out of band error', async () => {
252
+
253
+ mockSupergraphSdlRequest().reply(504);
254
+ mockOutOfBandReportRequestSuccess();
255
+
256
+ const fetcher = getDefaultFetcher();
257
+ await expect(
258
+ loadSupergraphSdlFromStorage({
259
+ graphRef,
260
+ apiKey,
261
+ endpoint: mockCloudConfigUrl1,
262
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
263
+ fetcher,
264
+ compositionId: null,
265
+ }),
266
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
267
+ `"An error occurred while fetching your schema from Apollo: 504 Gateway Timeout"`,
268
+ );
269
+ });
270
+
271
+ it('throws when there is no response and successfully submits an out of band error', async () => {
272
+ mockSupergraphSdlRequest().replyWithError('no response');
273
+ mockOutOfBandReportRequestSuccess();
274
+
275
+ const fetcher = getDefaultFetcher();
276
+ await expect(
277
+ loadSupergraphSdlFromStorage({
278
+ graphRef,
279
+ apiKey,
280
+ endpoint: mockCloudConfigUrl1,
281
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
282
+ fetcher,
283
+ compositionId: null,
284
+ }),
285
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
286
+ `"An error occurred while fetching your schema from Apollo: request to https://example1.cloud-config-url.com/cloudconfig/ failed, reason: no response"`,
287
+ );
288
+ });
289
+
290
+ it('throws on 502 status response and successfully submits an out of band error', async () => {
291
+ mockSupergraphSdlRequest().reply(502);
292
+ mockOutOfBandReportRequestSuccess();
293
+
294
+ const fetcher = getDefaultFetcher();
295
+ await expect(
296
+ loadSupergraphSdlFromStorage({
297
+ graphRef,
298
+ apiKey,
299
+ endpoint: mockCloudConfigUrl1,
300
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
301
+ fetcher,
302
+ compositionId: null,
303
+ }),
304
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
305
+ `"An error occurred while fetching your schema from Apollo: 502 Bad Gateway"`,
306
+ );
307
+ });
308
+
309
+ it('throws on 503 status response and successfully submits an out of band error', async () => {
310
+ mockSupergraphSdlRequest().reply(503);
311
+ mockOutOfBandReportRequestSuccess();
312
+
313
+ const fetcher = getDefaultFetcher();
314
+ await expect(
315
+ loadSupergraphSdlFromStorage({
316
+ graphRef,
317
+ apiKey,
318
+ endpoint: mockCloudConfigUrl1,
319
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
320
+ fetcher,
321
+ compositionId: null,
322
+ }),
323
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
324
+ `"An error occurred while fetching your schema from Apollo: 503 Service Unavailable"`,
325
+ );
326
+ });
327
+
328
+ it('successfully responds to SDL unchanged by returning null', async () => {
329
+ mockSupergraphSdlRequestIfAfterUnchanged("id-1234");
330
+
331
+ const fetcher = getDefaultFetcher();
332
+ const result = await loadSupergraphSdlFromStorage({
333
+ graphRef,
334
+ apiKey,
335
+ endpoint: mockCloudConfigUrl1,
336
+ errorReportingEndpoint: mockOutOfBandReporterUrl,
337
+ fetcher,
338
+ compositionId: "id-1234",
339
+ });
340
+ expect(result).toBeNull();
341
+ });
342
+ });
343
+
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../../tsconfig.test",
3
+ "include": ["**/*"],
4
+ "references": [
5
+ { "path": "../../../../" },
6
+ { "path": "../../../../../federation-integration-testsuite-js" },
7
+ ]
8
+ }
@@ -0,0 +1,128 @@
1
+ import { fetch } from 'apollo-server-env';
2
+ import { Logger } from 'apollo-server-types';
3
+ import resolvable from '@josephg/resolvable';
4
+ import { SupergraphManager, SupergraphSdlHookOptions } from '../../config';
5
+ import { SubgraphHealthCheckFunction, SupergraphSdlUpdateFunction } from '../..';
6
+ import { loadSupergraphSdlFromUplinks } from './loadSupergraphSdlFromStorage';
7
+
8
+ export interface UplinkFetcherOptions {
9
+ pollIntervalInMs: number;
10
+ subgraphHealthCheck?: boolean;
11
+ graphRef: string;
12
+ apiKey: string;
13
+ fetcher: typeof fetch;
14
+ maxRetries: number;
15
+ uplinkEndpoints: string[];
16
+ logger?: Logger;
17
+ }
18
+
19
+ type State =
20
+ | { phase: 'initialized' }
21
+ | { phase: 'polling'; pollingPromise?: Promise<void> }
22
+ | { phase: 'stopped' };
23
+
24
+ export class UplinkFetcher implements SupergraphManager {
25
+ private config: UplinkFetcherOptions;
26
+ private update?: SupergraphSdlUpdateFunction;
27
+ private healthCheck?: SubgraphHealthCheckFunction;
28
+ private timerRef: NodeJS.Timeout | null = null;
29
+ private state: State;
30
+ private errorReportingEndpoint: string | undefined =
31
+ process.env.APOLLO_OUT_OF_BAND_REPORTER_ENDPOINT ?? undefined;
32
+ private compositionId?: string;
33
+
34
+ constructor(options: UplinkFetcherOptions) {
35
+ this.config = options;
36
+ this.state = { phase: 'initialized' };
37
+ }
38
+
39
+ public async initialize({ update, healthCheck }: SupergraphSdlHookOptions) {
40
+ this.update = update;
41
+
42
+ if (this.config.subgraphHealthCheck) {
43
+ this.healthCheck = healthCheck;
44
+ }
45
+
46
+ let initialSupergraphSdl: string | null = null;
47
+ try {
48
+ initialSupergraphSdl = await this.updateSupergraphSdl();
49
+ } catch (e) {
50
+ this.logUpdateFailure(e);
51
+ throw e;
52
+ }
53
+
54
+ // Start polling after we resolve the first supergraph
55
+ this.beginPolling();
56
+
57
+ return {
58
+ // on init, this supergraphSdl should never actually be `null`.
59
+ // `this.updateSupergraphSdl()` will only return null if the schema hasn't
60
+ // changed over the course of an _update_.
61
+ supergraphSdl: initialSupergraphSdl!,
62
+ cleanup: async () => {
63
+ if (this.state.phase === 'polling') {
64
+ await this.state.pollingPromise;
65
+ }
66
+ this.state = { phase: 'stopped' };
67
+ if (this.timerRef) {
68
+ clearTimeout(this.timerRef);
69
+ this.timerRef = null;
70
+ }
71
+ },
72
+ };
73
+ }
74
+
75
+ private async updateSupergraphSdl() {
76
+ const result = await loadSupergraphSdlFromUplinks({
77
+ graphRef: this.config.graphRef,
78
+ apiKey: this.config.apiKey,
79
+ endpoints: this.config.uplinkEndpoints,
80
+ errorReportingEndpoint: this.errorReportingEndpoint,
81
+ fetcher: this.config.fetcher,
82
+ compositionId: this.compositionId ?? null,
83
+ maxRetries: this.config.maxRetries,
84
+ });
85
+
86
+ if (!result) {
87
+ return null;
88
+ } else {
89
+ this.compositionId = result.id;
90
+ // the healthCheck fn is only assigned if it's enabled in the config
91
+ await this.healthCheck?.(result.supergraphSdl);
92
+ return result.supergraphSdl;
93
+ }
94
+ }
95
+
96
+ private beginPolling() {
97
+ this.state = { phase: 'polling' };
98
+ this.poll();
99
+ }
100
+
101
+ private poll() {
102
+ this.timerRef = setTimeout(async () => {
103
+ if (this.state.phase === 'polling') {
104
+ const pollingPromise = resolvable();
105
+
106
+ this.state.pollingPromise = pollingPromise;
107
+ try {
108
+ const maybeNewSupergraphSdl = await this.updateSupergraphSdl();
109
+ if (maybeNewSupergraphSdl) {
110
+ this.update?.(maybeNewSupergraphSdl);
111
+ }
112
+ } catch (e) {
113
+ this.logUpdateFailure(e);
114
+ }
115
+ pollingPromise.resolve();
116
+ }
117
+
118
+ this.poll();
119
+ }, this.config.pollIntervalInMs);
120
+ }
121
+
122
+ private logUpdateFailure(e: any) {
123
+ this.config.logger?.error(
124
+ 'UplinkFetcher failed to update supergraph with the following error: ' +
125
+ (e.message ?? e),
126
+ );
127
+ }
128
+ }
@@ -1,12 +1,13 @@
1
1
  import { fetch, Response, Request } from 'apollo-server-env';
2
2
  import { GraphQLError } from 'graphql';
3
- import { OutOfBandReporter } from './outOfBandReporter';
4
- import { SupergraphSdlQuery } from './__generated__/graphqlTypes';
3
+ import { SupergraphSdlUpdate } from '../../config';
4
+ import { submitOutOfBandReportIfConfigured } from './outOfBandReporter';
5
+ import { SupergraphSdlQuery } from '../../__generated__/graphqlTypes';
5
6
 
6
7
  // Magic /* GraphQL */ comment below is for codegen, do not remove
7
8
  export const SUPERGRAPH_SDL_QUERY = /* GraphQL */`#graphql
8
- query SupergraphSdl($apiKey: String!, $ref: String!) {
9
- routerConfig(ref: $ref, apiKey: $apiKey) {
9
+ query SupergraphSdl($apiKey: String!, $ref: String!, $ifAfterId: ID) {
10
+ routerConfig(ref: $ref, apiKey: $apiKey, ifAfterId: $ifAfterId) {
10
11
  __typename
11
12
  ... on RouterConfigResult {
12
13
  id
@@ -34,21 +35,67 @@ interface SupergraphSdlQueryFailure {
34
35
  errors: GraphQLError[];
35
36
  }
36
37
 
37
- const { name, version } = require('../package.json');
38
+ const { name, version } = require('../../../package.json');
38
39
 
39
40
  const fetchErrorMsg = "An error occurred while fetching your schema from Apollo: ";
40
41
 
42
+ let fetchCounter = 0;
43
+
44
+ export async function loadSupergraphSdlFromUplinks({
45
+ graphRef,
46
+ apiKey,
47
+ endpoints,
48
+ errorReportingEndpoint,
49
+ fetcher,
50
+ compositionId,
51
+ maxRetries,
52
+ }: {
53
+ graphRef: string;
54
+ apiKey: string;
55
+ endpoints: string[];
56
+ errorReportingEndpoint: string | undefined,
57
+ fetcher: typeof fetch;
58
+ compositionId: string | null;
59
+ maxRetries: number
60
+ }) : Promise<SupergraphSdlUpdate | null> {
61
+ let retries = 0;
62
+ let lastException = null;
63
+ let result: SupergraphSdlUpdate | null = null;
64
+ while (retries++ <= maxRetries && result == null) {
65
+ try {
66
+ result = await loadSupergraphSdlFromStorage({
67
+ graphRef,
68
+ apiKey,
69
+ endpoint: endpoints[fetchCounter++ % endpoints.length],
70
+ errorReportingEndpoint,
71
+ fetcher,
72
+ compositionId
73
+ });
74
+ } catch (e) {
75
+ lastException = e;
76
+ }
77
+ }
78
+ if (result === null && lastException !== null) {
79
+ throw lastException;
80
+ }
81
+ return result;
82
+ }
83
+
41
84
  export async function loadSupergraphSdlFromStorage({
42
85
  graphRef,
43
86
  apiKey,
44
87
  endpoint,
88
+ errorReportingEndpoint,
45
89
  fetcher,
90
+ compositionId,
46
91
  }: {
47
92
  graphRef: string;
48
93
  apiKey: string;
49
94
  endpoint: string;
95
+ errorReportingEndpoint?: string;
50
96
  fetcher: typeof fetch;
51
- }) {
97
+ compositionId: string | null;
98
+ }) : Promise<SupergraphSdlUpdate | null> {
52
99
  let result: Response;
53
100
  const requestDetails = {
54
101
  method: 'POST',
@@ -57,6 +104,7 @@ export async function loadSupergraphSdlFromStorage({
57
104
  variables: {
58
105
  ref: graphRef,
59
106
  apiKey,
107
+ ifAfterId: compositionId,
60
108
  },
61
109
  }),
62
110
  headers: {
@@ -69,19 +117,19 @@ export async function loadSupergraphSdlFromStorage({
69
117
 
70
118
  const request: Request = new Request(endpoint, requestDetails);
71
119
 
72
- const OOBReport = new OutOfBandReporter();
73
- const startTime = new Date()
120
+ const startTime = new Date();
74
121
  try {
75
122
  result = await fetcher(endpoint, requestDetails);
76
123
  } catch (e) {
77
124
  const endTime = new Date();
78
125
 
79
- await OOBReport.submitOutOfBandReportIfConfigured({
126
+ await submitOutOfBandReportIfConfigured({
80
127
  error: e,
81
128
  request,
129
+ endpoint: errorReportingEndpoint,
82
130
  startedAt: startTime,
83
131
  endedAt: endTime,
84
- fetcher
132
+ fetcher,
85
133
  });
86
134
 
87
135
  throw new Error(fetchErrorMsg + (e.message ?? e));
@@ -106,13 +154,14 @@ export async function loadSupergraphSdlFromStorage({
106
154
  );
107
155
  }
108
156
  } else {
109
- await OOBReport.submitOutOfBandReportIfConfigured({
157
+ await submitOutOfBandReportIfConfigured({
110
158
  error: new Error(fetchErrorMsg + result.status + ' ' + result.statusText),
111
159
  request,
160
+ endpoint: errorReportingEndpoint,
112
161
  response: result,
113
162
  startedAt: startTime,
114
163
  endedAt: endTime,
115
- fetcher
164
+ fetcher,
116
165
  });
117
166
  throw new Error(fetchErrorMsg + result.status + ' ' + result.statusText);
118
167
  }
@@ -124,13 +173,13 @@ export async function loadSupergraphSdlFromStorage({
124
173
  supergraphSdl,
125
174
  // messages,
126
175
  } = routerConfig;
127
-
128
- // `supergraphSdl` should not be nullable in the schema, but it currently is
129
176
  return { id, supergraphSdl: supergraphSdl! };
130
177
  } else if (routerConfig.__typename === 'FetchError') {
131
178
  // FetchError case
132
179
  const { code, message } = routerConfig;
133
180
  throw new Error(`${code}: ${message}`);
181
+ } else if (routerConfig.__typename === 'Unchanged') {
182
+ return null;
134
183
  } else {
135
184
  throw new Error('Programming error: unhandled response failure');
136
185
  }