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