@apollo/gateway 0.45.1 → 0.46.0-alpha.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.
- package/CHANGELOG.md +21 -0
- package/dist/config.d.ts +42 -16
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +28 -18
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +35 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +205 -308
- package/dist/index.js.map +1 -1
- 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 +32 -0
- package/dist/supergraphManagers/UplinkFetcher/index.d.ts.map +1 -0
- package/dist/supergraphManagers/UplinkFetcher/index.js +96 -0
- package/dist/supergraphManagers/UplinkFetcher/index.js.map +1 -0
- package/dist/{loadSupergraphSdlFromStorage.d.ts → supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.d.ts} +1 -1
- package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.d.ts.map +1 -0
- package/dist/{loadSupergraphSdlFromStorage.js → supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.js} +1 -1
- package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.js.map +1 -0
- package/dist/{outOfBandReporter.d.ts → supergraphManagers/UplinkFetcher/outOfBandReporter.d.ts} +0 -0
- package/dist/supergraphManagers/UplinkFetcher/outOfBandReporter.d.ts.map +1 -0
- package/dist/{outOfBandReporter.js → supergraphManagers/UplinkFetcher/outOfBandReporter.js} +2 -2
- package/dist/supergraphManagers/UplinkFetcher/outOfBandReporter.js.map +1 -0
- package/dist/supergraphManagers/index.d.ts +5 -0
- package/dist/supergraphManagers/index.d.ts.map +1 -0
- package/dist/supergraphManagers/index.js +12 -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 +6 -4
- package/src/__tests__/execution-utils.ts +2 -2
- package/src/__tests__/gateway/buildService.test.ts +2 -2
- package/src/__tests__/gateway/lifecycle-hooks.test.ts +58 -99
- package/src/__tests__/gateway/opentelemetry.test.ts +8 -3
- package/src/__tests__/gateway/queryPlanCache.test.ts +25 -9
- package/src/__tests__/gateway/reporting.test.ts +4 -6
- package/src/__tests__/gateway/supergraphSdl.test.ts +390 -0
- package/src/__tests__/integration/aliases.test.ts +9 -3
- package/src/__tests__/integration/configuration.test.ts +109 -12
- package/src/__tests__/integration/logger.test.ts +1 -1
- package/src/__tests__/integration/networkRequests.test.ts +81 -118
- package/src/__tests__/integration/nockMocks.ts +15 -8
- package/src/config.ts +149 -40
- package/src/index.ts +314 -485
- 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 +163 -0
- package/src/{loadServicesFromRemoteEndpoint.ts → supergraphManagers/IntrospectAndCompose/loadServicesFromRemoteEndpoint.ts} +6 -6
- package/src/supergraphManagers/LegacyFetcher/index.ts +229 -0
- package/src/supergraphManagers/LocalCompose/index.ts +83 -0
- package/src/{__tests__ → supergraphManagers/UplinkFetcher/__tests__}/loadSupergraphSdlFromStorage.test.ts +4 -4
- package/src/supergraphManagers/UplinkFetcher/__tests__/tsconfig.json +8 -0
- package/src/supergraphManagers/UplinkFetcher/index.ts +128 -0
- package/src/{loadSupergraphSdlFromStorage.ts → supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts} +3 -3
- package/src/{outOfBandReporter.ts → supergraphManagers/UplinkFetcher/outOfBandReporter.ts} +2 -2
- package/src/supergraphManagers/index.ts +4 -0
- 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.map +0 -1
- package/dist/loadSupergraphSdlFromStorage.js.map +0 -1
- package/dist/outOfBandReporter.d.ts.map +0 -1
- package/dist/outOfBandReporter.js.map +0 -1
- package/src/__tests__/gateway/composedSdl.test.ts +0 -44
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import nock from 'nock';
|
|
2
|
+
import {
|
|
3
|
+
fixtures,
|
|
4
|
+
fixturesWithUpdate,
|
|
5
|
+
} from 'apollo-federation-integration-testsuite';
|
|
6
|
+
import { nockBeforeEach, nockAfterEach } from '../../../__tests__/nockAssertions';
|
|
7
|
+
import { RemoteGraphQLDataSource, ServiceEndpointDefinition } from '../../..';
|
|
8
|
+
import { IntrospectAndCompose } from '..';
|
|
9
|
+
import { mockAllServicesSdlQuerySuccess } from '../../../__tests__/integration/nockMocks';
|
|
10
|
+
import { getTestingSupergraphSdl, wait } from '../../../__tests__/execution-utils';
|
|
11
|
+
import resolvable from '@josephg/resolvable';
|
|
12
|
+
import { Logger } from 'apollo-server-types';
|
|
13
|
+
|
|
14
|
+
describe('IntrospectAndCompose', () => {
|
|
15
|
+
beforeEach(nockBeforeEach);
|
|
16
|
+
afterEach(nockAfterEach);
|
|
17
|
+
|
|
18
|
+
it('constructs', () => {
|
|
19
|
+
expect(
|
|
20
|
+
() =>
|
|
21
|
+
new IntrospectAndCompose({
|
|
22
|
+
subgraphs: fixtures,
|
|
23
|
+
}),
|
|
24
|
+
).not.toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('has an `initialize` property which is callable (simulating the gateway calling it)', async () => {
|
|
28
|
+
mockAllServicesSdlQuerySuccess();
|
|
29
|
+
const instance = new IntrospectAndCompose({ subgraphs: fixtures });
|
|
30
|
+
await expect(
|
|
31
|
+
instance.initialize({
|
|
32
|
+
update() {},
|
|
33
|
+
async healthCheck() {},
|
|
34
|
+
getDataSource({ url }) {
|
|
35
|
+
return new RemoteGraphQLDataSource({ url });
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
).resolves.toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('uses `GraphQLDataSource`s provided by the `buildService` function', async () => {
|
|
42
|
+
mockAllServicesSdlQuerySuccess();
|
|
43
|
+
|
|
44
|
+
const processSpies: jest.SpyInstance[] = [];
|
|
45
|
+
function getDataSourceSpy(definition: ServiceEndpointDefinition) {
|
|
46
|
+
const datasource = new RemoteGraphQLDataSource({
|
|
47
|
+
url: definition.url,
|
|
48
|
+
});
|
|
49
|
+
const processSpy = jest.spyOn(datasource, 'process');
|
|
50
|
+
processSpies.push(processSpy);
|
|
51
|
+
return datasource;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const instance = new IntrospectAndCompose({
|
|
55
|
+
subgraphs: fixtures,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await instance.initialize({
|
|
59
|
+
update() {},
|
|
60
|
+
async healthCheck() {},
|
|
61
|
+
getDataSource: getDataSourceSpy,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(processSpies.length).toBe(fixtures.length);
|
|
65
|
+
for (const processSpy of processSpies) {
|
|
66
|
+
expect(processSpy).toHaveBeenCalledTimes(1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('polls services when a `pollInterval` is set and stops when `cleanup` is called', async () => {
|
|
71
|
+
// This is mocked 4 times to include the initial load (followed by 3 polls)
|
|
72
|
+
// We need to alternate schemas, else the update will be ignored
|
|
73
|
+
mockAllServicesSdlQuerySuccess();
|
|
74
|
+
mockAllServicesSdlQuerySuccess(fixturesWithUpdate);
|
|
75
|
+
mockAllServicesSdlQuerySuccess();
|
|
76
|
+
mockAllServicesSdlQuerySuccess(fixturesWithUpdate);
|
|
77
|
+
|
|
78
|
+
const p1 = resolvable();
|
|
79
|
+
const p2 = resolvable();
|
|
80
|
+
const p3 = resolvable();
|
|
81
|
+
|
|
82
|
+
// `update` (below) is called each time we poll (and there's an update to
|
|
83
|
+
// the supergraph), so this is a reasonable hook into "when" the poll
|
|
84
|
+
// happens and drives this test cleanly with `Promise`s.
|
|
85
|
+
const updateSpy = jest
|
|
86
|
+
.fn()
|
|
87
|
+
.mockImplementationOnce(() => p1.resolve())
|
|
88
|
+
.mockImplementationOnce(() => p2.resolve())
|
|
89
|
+
.mockImplementationOnce(() => p3.resolve());
|
|
90
|
+
|
|
91
|
+
const instance = new IntrospectAndCompose({
|
|
92
|
+
subgraphs: fixtures,
|
|
93
|
+
pollIntervalInMs: 10,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const { cleanup } = await instance.initialize({
|
|
97
|
+
update(supergraphSdl) {
|
|
98
|
+
updateSpy(supergraphSdl);
|
|
99
|
+
},
|
|
100
|
+
async healthCheck() {},
|
|
101
|
+
getDataSource({ url }) {
|
|
102
|
+
return new RemoteGraphQLDataSource({ url });
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await Promise.all([p1, p2, p3]);
|
|
107
|
+
|
|
108
|
+
expect(updateSpy).toHaveBeenCalledTimes(3);
|
|
109
|
+
|
|
110
|
+
// stop polling
|
|
111
|
+
await cleanup!();
|
|
112
|
+
|
|
113
|
+
expect(updateSpy).toHaveBeenCalledTimes(3);
|
|
114
|
+
|
|
115
|
+
// ensure we cancelled the timer
|
|
116
|
+
// @ts-ignore
|
|
117
|
+
expect(instance.timerRef).toBe(null);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// TODO: useFakeTimers (though I'm struggling to get this to work as expected)
|
|
121
|
+
it("doesn't call `update` when there's no change to the supergraph", async () => {
|
|
122
|
+
const fetcher =
|
|
123
|
+
jest.requireActual<typeof import('apollo-server-env')>(
|
|
124
|
+
'apollo-server-env',
|
|
125
|
+
).fetch;
|
|
126
|
+
|
|
127
|
+
// mock for initial load and a few polls against an unchanging schema
|
|
128
|
+
mockAllServicesSdlQuerySuccess();
|
|
129
|
+
mockAllServicesSdlQuerySuccess();
|
|
130
|
+
mockAllServicesSdlQuerySuccess();
|
|
131
|
+
mockAllServicesSdlQuerySuccess();
|
|
132
|
+
|
|
133
|
+
const instance = new IntrospectAndCompose({
|
|
134
|
+
subgraphs: fixtures,
|
|
135
|
+
pollIntervalInMs: 100,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const updateSpy = jest.fn();
|
|
139
|
+
const { cleanup } = await instance.initialize({
|
|
140
|
+
update(supergraphSdl) {
|
|
141
|
+
updateSpy(supergraphSdl);
|
|
142
|
+
},
|
|
143
|
+
async healthCheck() {},
|
|
144
|
+
getDataSource({ url }) {
|
|
145
|
+
return new RemoteGraphQLDataSource({
|
|
146
|
+
url,
|
|
147
|
+
fetcher,
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// let the instance poll through all the active mocks
|
|
153
|
+
// wouldn't need to do this if I could get fakeTimers working as expected
|
|
154
|
+
while (nock.activeMocks().length > 0) {
|
|
155
|
+
await wait(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await cleanup!();
|
|
159
|
+
|
|
160
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('issues subgraph health checks when enabled (and polling)', async () => {
|
|
164
|
+
mockAllServicesSdlQuerySuccess();
|
|
165
|
+
mockAllServicesSdlQuerySuccess(fixturesWithUpdate);
|
|
166
|
+
|
|
167
|
+
const healthCheckPromiseOnLoad = resolvable();
|
|
168
|
+
const healthCheckPromiseOnUpdate = resolvable();
|
|
169
|
+
|
|
170
|
+
const healthCheckSpy = jest
|
|
171
|
+
.fn()
|
|
172
|
+
.mockImplementationOnce(() => healthCheckPromiseOnLoad.resolve())
|
|
173
|
+
.mockImplementationOnce(() => healthCheckPromiseOnUpdate.resolve());
|
|
174
|
+
|
|
175
|
+
const instance = new IntrospectAndCompose({
|
|
176
|
+
subgraphs: fixtures,
|
|
177
|
+
pollIntervalInMs: 10,
|
|
178
|
+
subgraphHealthCheck: true,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const { cleanup } = await instance.initialize({
|
|
182
|
+
update() {},
|
|
183
|
+
async healthCheck(supergraphSdl) {
|
|
184
|
+
healthCheckSpy(supergraphSdl);
|
|
185
|
+
},
|
|
186
|
+
getDataSource({ url }) {
|
|
187
|
+
return new RemoteGraphQLDataSource({ url });
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await Promise.all([
|
|
192
|
+
healthCheckPromiseOnLoad,
|
|
193
|
+
healthCheckPromiseOnUpdate,
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
expect(healthCheckSpy).toHaveBeenNthCalledWith(
|
|
197
|
+
1,
|
|
198
|
+
getTestingSupergraphSdl(fixtures),
|
|
199
|
+
);
|
|
200
|
+
expect(healthCheckSpy).toHaveBeenNthCalledWith(
|
|
201
|
+
2,
|
|
202
|
+
getTestingSupergraphSdl(fixturesWithUpdate),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// stop polling
|
|
206
|
+
await cleanup!();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('errors', () => {
|
|
210
|
+
it('logs an error when `update` function throws', async () => {
|
|
211
|
+
const errorLoggedPromise = resolvable();
|
|
212
|
+
|
|
213
|
+
const errorSpy = jest.fn(() => {
|
|
214
|
+
errorLoggedPromise.resolve();
|
|
215
|
+
});
|
|
216
|
+
const logger: Logger = {
|
|
217
|
+
error: errorSpy,
|
|
218
|
+
debug() {},
|
|
219
|
+
info() {},
|
|
220
|
+
warn() {},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// mock successful initial load
|
|
224
|
+
mockAllServicesSdlQuerySuccess();
|
|
225
|
+
|
|
226
|
+
// mock first update
|
|
227
|
+
mockAllServicesSdlQuerySuccess(fixturesWithUpdate);
|
|
228
|
+
|
|
229
|
+
const instance = new IntrospectAndCompose({
|
|
230
|
+
subgraphs: fixtures,
|
|
231
|
+
pollIntervalInMs: 1000,
|
|
232
|
+
logger,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const thrownErrorMessage = 'invalid supergraph';
|
|
236
|
+
// simulate gateway throwing an error when `update` is called
|
|
237
|
+
const updateSpy = jest.fn().mockImplementationOnce(() => {
|
|
238
|
+
throw new Error(thrownErrorMessage);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const { cleanup } = await instance.initialize({
|
|
242
|
+
update: updateSpy,
|
|
243
|
+
async healthCheck() {},
|
|
244
|
+
getDataSource({ url }) {
|
|
245
|
+
return new RemoteGraphQLDataSource({ url });
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await errorLoggedPromise;
|
|
250
|
+
// stop polling
|
|
251
|
+
await cleanup!();
|
|
252
|
+
|
|
253
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
254
|
+
expect(logger.error).toHaveBeenCalledTimes(1);
|
|
255
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
256
|
+
`IntrospectAndCompose failed to update supergraph with the following error: ${thrownErrorMessage}`,
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('fails to load when `healthCheck` function throws on startup', async () => {
|
|
261
|
+
mockAllServicesSdlQuerySuccess();
|
|
262
|
+
|
|
263
|
+
const expectedErrorMsg = 'error reaching subgraph';
|
|
264
|
+
const errorLoggedPromise = resolvable();
|
|
265
|
+
const errorSpy = jest.fn(() => {
|
|
266
|
+
errorLoggedPromise.resolve();
|
|
267
|
+
});
|
|
268
|
+
const logger: Logger = {
|
|
269
|
+
error: errorSpy,
|
|
270
|
+
debug() {},
|
|
271
|
+
info() {},
|
|
272
|
+
warn() {},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const updateSpy = jest.fn();
|
|
276
|
+
|
|
277
|
+
const instance = new IntrospectAndCompose({
|
|
278
|
+
subgraphs: fixtures,
|
|
279
|
+
pollIntervalInMs: 10,
|
|
280
|
+
subgraphHealthCheck: true,
|
|
281
|
+
logger,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await expect(
|
|
285
|
+
instance.initialize({
|
|
286
|
+
update() {
|
|
287
|
+
updateSpy();
|
|
288
|
+
},
|
|
289
|
+
async healthCheck() {
|
|
290
|
+
throw new Error(expectedErrorMsg);
|
|
291
|
+
},
|
|
292
|
+
getDataSource({ url }) {
|
|
293
|
+
return new RemoteGraphQLDataSource({ url });
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`"error reaching subgraph"`);
|
|
297
|
+
|
|
298
|
+
await errorLoggedPromise;
|
|
299
|
+
|
|
300
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
301
|
+
`IntrospectAndCompose failed to update supergraph with the following error: ${expectedErrorMsg}`,
|
|
302
|
+
);
|
|
303
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('does not attempt to update when `healthCheck` function throws', async () => {
|
|
307
|
+
mockAllServicesSdlQuerySuccess();
|
|
308
|
+
mockAllServicesSdlQuerySuccess(fixturesWithUpdate);
|
|
309
|
+
|
|
310
|
+
const expectedErrorMsg = 'error reaching subgraph';
|
|
311
|
+
const errorLoggedPromise = resolvable();
|
|
312
|
+
const errorSpy = jest.fn(() => {
|
|
313
|
+
errorLoggedPromise.resolve();
|
|
314
|
+
});
|
|
315
|
+
const logger: Logger = {
|
|
316
|
+
error: errorSpy,
|
|
317
|
+
debug() {},
|
|
318
|
+
info() {},
|
|
319
|
+
warn() {},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const healthCheckPromiseOnLoad = resolvable();
|
|
323
|
+
const healthCheckPromiseOnUpdate = resolvable();
|
|
324
|
+
const healthCheckSpyWhichEventuallyThrows = jest
|
|
325
|
+
.fn()
|
|
326
|
+
.mockImplementationOnce(() => healthCheckPromiseOnLoad.resolve())
|
|
327
|
+
.mockImplementationOnce(() => {
|
|
328
|
+
healthCheckPromiseOnUpdate.resolve();
|
|
329
|
+
throw new Error(expectedErrorMsg);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const updateSpy = jest.fn();
|
|
333
|
+
|
|
334
|
+
const instance = new IntrospectAndCompose({
|
|
335
|
+
subgraphs: fixtures,
|
|
336
|
+
pollIntervalInMs: 10,
|
|
337
|
+
subgraphHealthCheck: true,
|
|
338
|
+
logger,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const { cleanup } = await instance.initialize({
|
|
342
|
+
update() {
|
|
343
|
+
updateSpy();
|
|
344
|
+
},
|
|
345
|
+
async healthCheck(supergraphSdl) {
|
|
346
|
+
healthCheckSpyWhichEventuallyThrows(supergraphSdl);
|
|
347
|
+
},
|
|
348
|
+
getDataSource({ url }) {
|
|
349
|
+
return new RemoteGraphQLDataSource({ url });
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await Promise.all([
|
|
354
|
+
healthCheckPromiseOnLoad,
|
|
355
|
+
healthCheckPromiseOnUpdate,
|
|
356
|
+
errorLoggedPromise,
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
360
|
+
`IntrospectAndCompose failed to update supergraph with the following error: ${expectedErrorMsg}`,
|
|
361
|
+
);
|
|
362
|
+
expect(healthCheckSpyWhichEventuallyThrows).toHaveBeenCalledTimes(2);
|
|
363
|
+
// update isn't called on load so this shouldn't be called even once
|
|
364
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
365
|
+
|
|
366
|
+
// stop polling
|
|
367
|
+
await cleanup!();
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RemoteGraphQLDataSource } from '
|
|
1
|
+
import { loadServicesFromRemoteEndpoint } from '../loadServicesFromRemoteEndpoint';
|
|
2
|
+
import { RemoteGraphQLDataSource } from '../../../datasources';
|
|
3
3
|
|
|
4
|
-
describe('
|
|
4
|
+
describe('loadServicesFromRemoteEndpoint', () => {
|
|
5
5
|
it('errors when no URL was specified', async () => {
|
|
6
6
|
const serviceSdlCache = new Map<string, string>();
|
|
7
7
|
const dataSource = new RemoteGraphQLDataSource({ url: '' });
|
|
8
8
|
const serviceList = [{ name: 'test', dataSource }];
|
|
9
9
|
await expect(
|
|
10
|
-
|
|
10
|
+
loadServicesFromRemoteEndpoint({
|
|
11
11
|
serviceList,
|
|
12
12
|
serviceSdlCache,
|
|
13
13
|
getServiceIntrospectionHeaders: async () => ({})
|
|
@@ -28,7 +28,7 @@ describe('getServiceDefinitionsFromRemoteEndpoint', () => {
|
|
|
28
28
|
// of `EAI_AGAIN` or `ENOTFOUND`. This `toThrowError` uses a Regex
|
|
29
29
|
// to match either case.
|
|
30
30
|
await expect(
|
|
31
|
-
|
|
31
|
+
loadServicesFromRemoteEndpoint({
|
|
32
32
|
serviceList,
|
|
33
33
|
serviceSdlCache,
|
|
34
34
|
getServiceIntrospectionHeaders: async () => ({}),
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
composeAndValidate,
|
|
3
|
+
compositionHasErrors,
|
|
4
|
+
ServiceDefinition,
|
|
5
|
+
} from '@apollo/federation';
|
|
6
|
+
import { Logger } from 'apollo-server-types';
|
|
7
|
+
import { HeadersInit } from 'node-fetch';
|
|
8
|
+
import resolvable from '@josephg/resolvable';
|
|
9
|
+
import {
|
|
10
|
+
ServiceEndpointDefinition,
|
|
11
|
+
SupergraphSdlUpdateFunction,
|
|
12
|
+
SubgraphHealthCheckFunction,
|
|
13
|
+
} from '../..';
|
|
14
|
+
import {
|
|
15
|
+
loadServicesFromRemoteEndpoint,
|
|
16
|
+
Service,
|
|
17
|
+
} from './loadServicesFromRemoteEndpoint';
|
|
18
|
+
import { SupergraphManager, SupergraphSdlHookOptions } from '../../config';
|
|
19
|
+
|
|
20
|
+
export interface IntrospectAndComposeOptions {
|
|
21
|
+
subgraphs: ServiceEndpointDefinition[];
|
|
22
|
+
introspectionHeaders?:
|
|
23
|
+
| HeadersInit
|
|
24
|
+
| ((
|
|
25
|
+
service: ServiceEndpointDefinition,
|
|
26
|
+
) => Promise<HeadersInit> | HeadersInit);
|
|
27
|
+
pollIntervalInMs?: number;
|
|
28
|
+
logger?: Logger;
|
|
29
|
+
subgraphHealthCheck?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type State =
|
|
33
|
+
| { phase: 'initialized' }
|
|
34
|
+
| { phase: 'polling'; pollingPromise?: Promise<void> }
|
|
35
|
+
| { phase: 'stopped' };
|
|
36
|
+
|
|
37
|
+
export class IntrospectAndCompose implements SupergraphManager {
|
|
38
|
+
private config: IntrospectAndComposeOptions;
|
|
39
|
+
private update?: SupergraphSdlUpdateFunction;
|
|
40
|
+
private healthCheck?: SubgraphHealthCheckFunction;
|
|
41
|
+
private subgraphs?: Service[];
|
|
42
|
+
private serviceSdlCache: Map<string, string> = new Map();
|
|
43
|
+
private timerRef: NodeJS.Timeout | null = null;
|
|
44
|
+
private state: State;
|
|
45
|
+
|
|
46
|
+
constructor(options: IntrospectAndComposeOptions) {
|
|
47
|
+
this.config = options;
|
|
48
|
+
this.state = { phase: 'initialized' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async initialize({ update, getDataSource, healthCheck }: SupergraphSdlHookOptions) {
|
|
52
|
+
this.update = update;
|
|
53
|
+
|
|
54
|
+
if (this.config.subgraphHealthCheck) {
|
|
55
|
+
this.healthCheck = healthCheck;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.subgraphs = this.config.subgraphs.map((subgraph) => ({
|
|
59
|
+
...subgraph,
|
|
60
|
+
dataSource: getDataSource(subgraph),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
let initialSupergraphSdl: string | null = null;
|
|
64
|
+
try {
|
|
65
|
+
initialSupergraphSdl = await this.updateSupergraphSdl();
|
|
66
|
+
} catch (e) {
|
|
67
|
+
this.logUpdateFailure(e);
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Start polling after we resolve the first supergraph
|
|
72
|
+
if (this.config.pollIntervalInMs) {
|
|
73
|
+
this.beginPolling();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
// on init, this supergraphSdl should never actually be `null`.
|
|
78
|
+
// `this.updateSupergraphSdl()` will only return null if the schema hasn't
|
|
79
|
+
// changed over the course of an _update_.
|
|
80
|
+
supergraphSdl: initialSupergraphSdl!,
|
|
81
|
+
cleanup: async () => {
|
|
82
|
+
if (this.state.phase === 'polling') {
|
|
83
|
+
await this.state.pollingPromise;
|
|
84
|
+
}
|
|
85
|
+
this.state = { phase: 'stopped' };
|
|
86
|
+
if (this.timerRef) {
|
|
87
|
+
clearTimeout(this.timerRef);
|
|
88
|
+
this.timerRef = null;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async updateSupergraphSdl() {
|
|
95
|
+
const result = await loadServicesFromRemoteEndpoint({
|
|
96
|
+
serviceList: this.subgraphs!,
|
|
97
|
+
getServiceIntrospectionHeaders: async (service) => {
|
|
98
|
+
return typeof this.config.introspectionHeaders === 'function'
|
|
99
|
+
? await this.config.introspectionHeaders(service)
|
|
100
|
+
: this.config.introspectionHeaders;
|
|
101
|
+
},
|
|
102
|
+
serviceSdlCache: this.serviceSdlCache,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!result.isNewSchema) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const supergraphSdl = this.createSupergraphFromSubgraphList(result.serviceDefinitions!);
|
|
110
|
+
// the healthCheck fn is only assigned if it's enabled in the config
|
|
111
|
+
await this.healthCheck?.(supergraphSdl);
|
|
112
|
+
|
|
113
|
+
return supergraphSdl;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private createSupergraphFromSubgraphList(subgraphs: ServiceDefinition[]) {
|
|
117
|
+
const compositionResult = composeAndValidate(subgraphs);
|
|
118
|
+
|
|
119
|
+
if (compositionHasErrors(compositionResult)) {
|
|
120
|
+
const { errors } = compositionResult;
|
|
121
|
+
throw Error(
|
|
122
|
+
"A valid schema couldn't be composed. The following composition errors were found:\n" +
|
|
123
|
+
errors.map((e) => '\t' + e.message).join('\n'),
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
const { supergraphSdl } = compositionResult;
|
|
127
|
+
return supergraphSdl;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private beginPolling() {
|
|
132
|
+
this.state = { phase: 'polling' };
|
|
133
|
+
this.poll();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private poll() {
|
|
137
|
+
this.timerRef = setTimeout(async () => {
|
|
138
|
+
if (this.state.phase === 'polling') {
|
|
139
|
+
const pollingPromise = resolvable();
|
|
140
|
+
|
|
141
|
+
this.state.pollingPromise = pollingPromise;
|
|
142
|
+
try {
|
|
143
|
+
const maybeNewSupergraphSdl = await this.updateSupergraphSdl();
|
|
144
|
+
if (maybeNewSupergraphSdl) {
|
|
145
|
+
this.update?.(maybeNewSupergraphSdl);
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
this.logUpdateFailure(e);
|
|
149
|
+
}
|
|
150
|
+
pollingPromise.resolve();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.poll();
|
|
154
|
+
}, this.config.pollIntervalInMs!);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private logUpdateFailure(e: any) {
|
|
158
|
+
this.config.logger?.error(
|
|
159
|
+
'IntrospectAndCompose failed to update supergraph with the following error: ' +
|
|
160
|
+
(e.message ?? e),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { GraphQLRequest } from 'apollo-server-types';
|
|
2
2
|
import { parse } from 'graphql';
|
|
3
3
|
import { Headers, HeadersInit } from 'node-fetch';
|
|
4
|
-
import { GraphQLDataSource, GraphQLDataSourceRequestKind } from '
|
|
5
|
-
import { SERVICE_DEFINITION_QUERY } from '
|
|
6
|
-
import {
|
|
4
|
+
import { GraphQLDataSource, GraphQLDataSourceRequestKind } from '../../datasources/types';
|
|
5
|
+
import { SERVICE_DEFINITION_QUERY } from '../..';
|
|
6
|
+
import { ServiceDefinitionUpdate, ServiceEndpointDefinition } from '../../config';
|
|
7
7
|
import { ServiceDefinition } from '@apollo/federation';
|
|
8
8
|
|
|
9
|
-
type Service = ServiceEndpointDefinition & {
|
|
9
|
+
export type Service = ServiceEndpointDefinition & {
|
|
10
10
|
dataSource: GraphQLDataSource;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export async function
|
|
13
|
+
export async function loadServicesFromRemoteEndpoint({
|
|
14
14
|
serviceList,
|
|
15
15
|
getServiceIntrospectionHeaders,
|
|
16
16
|
serviceSdlCache,
|
|
@@ -20,7 +20,7 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
|
|
|
20
20
|
service: ServiceEndpointDefinition,
|
|
21
21
|
) => Promise<HeadersInit | undefined>;
|
|
22
22
|
serviceSdlCache: Map<string, string>;
|
|
23
|
-
}): Promise<
|
|
23
|
+
}): Promise<ServiceDefinitionUpdate> {
|
|
24
24
|
if (!serviceList || !serviceList.length) {
|
|
25
25
|
throw new Error(
|
|
26
26
|
'Tried to load services from remote endpoints but none provided',
|