@apollo/gateway 0.35.0-alpha.1 → 0.37.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 +15 -0
- package/dist/__generated__/graphqlTypes.d.ts +12 -10
- package/dist/__generated__/graphqlTypes.d.ts.map +1 -1
- package/dist/datasources/LocalGraphQLDataSource.d.ts +3 -3
- package/dist/datasources/LocalGraphQLDataSource.d.ts.map +1 -1
- package/dist/datasources/LocalGraphQLDataSource.js +1 -1
- package/dist/datasources/LocalGraphQLDataSource.js.map +1 -1
- package/dist/datasources/RemoteGraphQLDataSource.d.ts +5 -3
- package/dist/datasources/RemoteGraphQLDataSource.d.ts.map +1 -1
- package/dist/datasources/RemoteGraphQLDataSource.js +51 -12
- package/dist/datasources/RemoteGraphQLDataSource.js.map +1 -1
- package/dist/datasources/index.d.ts +1 -1
- package/dist/datasources/index.d.ts.map +1 -1
- package/dist/datasources/parseCacheControlHeader.d.ts +2 -0
- package/dist/datasources/parseCacheControlHeader.d.ts.map +1 -0
- package/dist/datasources/parseCacheControlHeader.js +16 -0
- package/dist/datasources/parseCacheControlHeader.js.map +1 -0
- package/dist/datasources/types.d.ts +16 -1
- package/dist/datasources/types.d.ts.map +1 -1
- package/dist/datasources/types.js +7 -0
- package/dist/datasources/types.js.map +1 -1
- package/dist/executeQueryPlan.d.ts.map +1 -1
- package/dist/executeQueryPlan.js +3 -0
- package/dist/executeQueryPlan.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/loadServicesFromRemoteEndpoint.d.ts.map +1 -1
- package/dist/loadServicesFromRemoteEndpoint.js +9 -4
- package/dist/loadServicesFromRemoteEndpoint.js.map +1 -1
- package/package.json +8 -8
- package/src/__generated__/graphqlTypes.ts +2 -15
- package/src/__tests__/gateway/__snapshots__/opentelemetry.test.ts.snap +12 -4
- package/src/__tests__/gateway/buildService.test.ts +8 -2
- package/src/__tests__/gateway/endToEnd.test.ts +166 -0
- package/src/__tests__/gateway/opentelemetry.test.ts +7 -6
- package/src/__tests__/gateway/reporting.test.ts +4 -0
- package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +5 -0
- package/src/datasources/LocalGraphQLDataSource.ts +9 -10
- package/src/datasources/RemoteGraphQLDataSource.ts +106 -38
- package/src/datasources/__tests__/LocalGraphQLDataSource.test.ts +7 -1
- package/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +38 -18
- package/src/datasources/index.ts +1 -1
- package/src/datasources/parseCacheControlHeader.ts +43 -0
- package/src/datasources/types.ts +47 -2
- package/src/executeQueryPlan.ts +3 -1
- package/src/index.ts +11 -2
- package/src/loadServicesFromRemoteEndpoint.ts +9 -5
|
@@ -3,23 +3,25 @@ import {
|
|
|
3
3
|
GraphQLResponse,
|
|
4
4
|
ValueOrPromise,
|
|
5
5
|
GraphQLRequest,
|
|
6
|
+
CacheHint,
|
|
7
|
+
CacheScope,
|
|
8
|
+
CachePolicy,
|
|
6
9
|
} from 'apollo-server-types';
|
|
7
10
|
import {
|
|
8
11
|
ApolloError,
|
|
9
12
|
AuthenticationError,
|
|
10
13
|
ForbiddenError,
|
|
11
14
|
} from 'apollo-server-errors';
|
|
12
|
-
import {
|
|
13
|
-
fetch,
|
|
14
|
-
Request,
|
|
15
|
-
Headers,
|
|
16
|
-
Response,
|
|
17
|
-
} from 'apollo-server-env';
|
|
15
|
+
import { fetch, Request, Headers, Response } from 'apollo-server-env';
|
|
18
16
|
import { isObject } from '../utilities/predicates';
|
|
19
|
-
import { GraphQLDataSource } from './types';
|
|
17
|
+
import { GraphQLDataSource, GraphQLDataSourceProcessOptions, GraphQLDataSourceRequestKind } from './types';
|
|
20
18
|
import createSHA from 'apollo-server-core/dist/utils/createSHA';
|
|
19
|
+
import { parseCacheControlHeader } from './parseCacheControlHeader';
|
|
21
20
|
|
|
22
|
-
export class RemoteGraphQLDataSource<
|
|
21
|
+
export class RemoteGraphQLDataSource<
|
|
22
|
+
TContext extends Record<string, any> = Record<string, any>,
|
|
23
|
+
> implements GraphQLDataSource<TContext>
|
|
24
|
+
{
|
|
23
25
|
fetcher: typeof fetch = fetch;
|
|
24
26
|
|
|
25
27
|
constructor(
|
|
@@ -54,12 +56,25 @@ export class RemoteGraphQLDataSource<TContext extends Record<string, any> = Reco
|
|
|
54
56
|
*/
|
|
55
57
|
apq: boolean = false;
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Should cache-control response headers from subgraphs affect the operation's
|
|
61
|
+
* cache policy? If it shouldn't, set this to false.
|
|
62
|
+
*/
|
|
63
|
+
honorSubgraphCacheControlHeader: boolean = true;
|
|
64
|
+
|
|
65
|
+
async process(
|
|
66
|
+
options: GraphQLDataSourceProcessOptions<TContext>,
|
|
67
|
+
): Promise<GraphQLResponse> {
|
|
68
|
+
const { request, context: originalContext } = options;
|
|
69
|
+
// Deal with a bit of a hairy situation in typings: when doing health checks
|
|
70
|
+
// and schema checks we always pass in `{}` as the context even though it's
|
|
71
|
+
// not really guaranteed to be a `TContext`, and then we pass it to various
|
|
72
|
+
// methods on this object. The reason this "works" is that the DataSourceMap
|
|
73
|
+
// and Service types aren't generic-ized on TContext at all (so `{}` is in
|
|
74
|
+
// practice always legal there)... ie, the genericness of this class is
|
|
75
|
+
// questionable in the first place.
|
|
76
|
+
const context = originalContext as TContext;
|
|
77
|
+
|
|
63
78
|
// Respect incoming http headers (eg, apollo-federation-include-trace).
|
|
64
79
|
const headers = (request.http && request.http.headers) || new Headers();
|
|
65
80
|
headers.set('Content-Type', 'application/json');
|
|
@@ -71,24 +86,27 @@ export class RemoteGraphQLDataSource<TContext extends Record<string, any> = Reco
|
|
|
71
86
|
};
|
|
72
87
|
|
|
73
88
|
if (this.willSendRequest) {
|
|
74
|
-
await this.willSendRequest(
|
|
89
|
+
await this.willSendRequest(options);
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
if (!request.query) {
|
|
78
|
-
throw new Error(
|
|
93
|
+
throw new Error('Missing query');
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
const { query, ...requestWithoutQuery } = request;
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
// Special handling of cache-control headers in response. Requires
|
|
99
|
+
// Apollo Server 3, so we check to make sure the method we want is
|
|
100
|
+
// there.
|
|
101
|
+
const overallCachePolicy =
|
|
102
|
+
this.honorSubgraphCacheControlHeader &&
|
|
103
|
+
options.kind === GraphQLDataSourceRequestKind.INCOMING_OPERATION &&
|
|
104
|
+
options.incomingRequestContext.overallCachePolicy?.restrict
|
|
105
|
+
? options.incomingRequestContext.overallCachePolicy
|
|
106
|
+
: null;
|
|
87
107
|
|
|
88
108
|
if (this.apq) {
|
|
89
|
-
const apqHash = createSHA('sha256')
|
|
90
|
-
.update(request.query)
|
|
91
|
-
.digest('hex');
|
|
109
|
+
const apqHash = createSHA('sha256').update(request.query).digest('hex');
|
|
92
110
|
|
|
93
111
|
// Take the original extensions and extend them with
|
|
94
112
|
// the necessary "extensions" for APQ handshaking.
|
|
@@ -100,17 +118,25 @@ export class RemoteGraphQLDataSource<TContext extends Record<string, any> = Reco
|
|
|
100
118
|
},
|
|
101
119
|
};
|
|
102
120
|
|
|
103
|
-
const apqOptimisticResponse =
|
|
104
|
-
|
|
121
|
+
const apqOptimisticResponse = await this.sendRequest(
|
|
122
|
+
requestWithoutQuery,
|
|
123
|
+
context,
|
|
124
|
+
);
|
|
105
125
|
|
|
106
126
|
// If we didn't receive notice to retry with APQ, then let's
|
|
107
127
|
// assume this is the best result we'll get and return it!
|
|
108
128
|
if (
|
|
109
129
|
!apqOptimisticResponse.errors ||
|
|
110
|
-
!apqOptimisticResponse.errors.find(
|
|
111
|
-
error.message === 'PersistedQueryNotFound'
|
|
130
|
+
!apqOptimisticResponse.errors.find(
|
|
131
|
+
(error) => error.message === 'PersistedQueryNotFound',
|
|
132
|
+
)
|
|
112
133
|
) {
|
|
113
|
-
return respond(
|
|
134
|
+
return this.respond({
|
|
135
|
+
response: apqOptimisticResponse,
|
|
136
|
+
request: requestWithoutQuery,
|
|
137
|
+
context,
|
|
138
|
+
overallCachePolicy,
|
|
139
|
+
});
|
|
114
140
|
}
|
|
115
141
|
}
|
|
116
142
|
|
|
@@ -122,18 +148,22 @@ export class RemoteGraphQLDataSource<TContext extends Record<string, any> = Reco
|
|
|
122
148
|
...requestWithoutQuery,
|
|
123
149
|
};
|
|
124
150
|
const response = await this.sendRequest(requestWithQuery, context);
|
|
125
|
-
return respond(
|
|
151
|
+
return this.respond({
|
|
152
|
+
response,
|
|
153
|
+
request: requestWithQuery,
|
|
154
|
+
context,
|
|
155
|
+
overallCachePolicy,
|
|
156
|
+
});
|
|
126
157
|
}
|
|
127
158
|
|
|
128
159
|
private async sendRequest(
|
|
129
160
|
request: GraphQLRequest,
|
|
130
161
|
context: TContext,
|
|
131
162
|
): Promise<GraphQLResponse> {
|
|
132
|
-
|
|
133
163
|
// This would represent an internal programming error since this shouldn't
|
|
134
164
|
// be possible in the way that this method is invoked right now.
|
|
135
165
|
if (!request.http) {
|
|
136
|
-
throw new Error("Internal error: Only 'http' requests are supported.")
|
|
166
|
+
throw new Error("Internal error: Only 'http' requests are supported.");
|
|
137
167
|
}
|
|
138
168
|
|
|
139
169
|
// We don't want to serialize the `http` properties into the body that is
|
|
@@ -177,16 +207,54 @@ export class RemoteGraphQLDataSource<TContext extends Record<string, any> = Reco
|
|
|
177
207
|
}
|
|
178
208
|
|
|
179
209
|
public willSendRequest?(
|
|
180
|
-
|
|
181
|
-
GraphQLRequestContext<TContext>,
|
|
182
|
-
'request' | 'context'
|
|
183
|
-
>,
|
|
210
|
+
options: GraphQLDataSourceProcessOptions<TContext>,
|
|
184
211
|
): ValueOrPromise<void>;
|
|
185
212
|
|
|
213
|
+
private async respond({
|
|
214
|
+
response,
|
|
215
|
+
request,
|
|
216
|
+
context,
|
|
217
|
+
overallCachePolicy,
|
|
218
|
+
}: {
|
|
219
|
+
response: GraphQLResponse;
|
|
220
|
+
request: GraphQLRequest;
|
|
221
|
+
context: TContext;
|
|
222
|
+
overallCachePolicy: CachePolicy | null;
|
|
223
|
+
}): Promise<GraphQLResponse> {
|
|
224
|
+
const processedResponse =
|
|
225
|
+
typeof this.didReceiveResponse === 'function'
|
|
226
|
+
? await this.didReceiveResponse({ response, request, context })
|
|
227
|
+
: response;
|
|
228
|
+
|
|
229
|
+
if (overallCachePolicy) {
|
|
230
|
+
const parsed = parseCacheControlHeader(
|
|
231
|
+
response.http?.headers.get('cache-control'),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// If the subgraph does not specify a max-age, we assume its response (and
|
|
235
|
+
// thus the overall response) is uncacheable. (If you don't like this, you
|
|
236
|
+
// can tweak the `cache-control` header in your `didReceiveResponse`
|
|
237
|
+
// method.)
|
|
238
|
+
const hint: CacheHint = { maxAge: 0 };
|
|
239
|
+
const maxAge = parsed['max-age'];
|
|
240
|
+
if (typeof maxAge === 'string' && maxAge.match(/^[0-9]+$/)) {
|
|
241
|
+
hint.maxAge = +maxAge;
|
|
242
|
+
}
|
|
243
|
+
if (parsed['private'] === true) {
|
|
244
|
+
hint.scope = CacheScope.Private;
|
|
245
|
+
}
|
|
246
|
+
if (parsed['public'] === true) {
|
|
247
|
+
hint.scope = CacheScope.Public;
|
|
248
|
+
}
|
|
249
|
+
overallCachePolicy.restrict(hint);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return processedResponse;
|
|
253
|
+
}
|
|
254
|
+
|
|
186
255
|
public didReceiveResponse?(
|
|
187
|
-
requestContext: Required<
|
|
188
|
-
GraphQLRequestContext<TContext>,
|
|
189
|
-
'request' | 'response' | 'context'>
|
|
256
|
+
requestContext: Required<
|
|
257
|
+
Pick<GraphQLRequestContext<TContext>, 'request' | 'response' | 'context'>
|
|
190
258
|
>,
|
|
191
259
|
): ValueOrPromise<GraphQLResponse>;
|
|
192
260
|
|
|
@@ -2,6 +2,8 @@ import { LocalGraphQLDataSource } from '../LocalGraphQLDataSource';
|
|
|
2
2
|
import { buildFederatedSchema } from '@apollo/federation';
|
|
3
3
|
import gql from 'graphql-tag';
|
|
4
4
|
import { GraphQLResolverMap } from 'apollo-graphql';
|
|
5
|
+
import { GraphQLRequestContext } from 'apollo-server-types';
|
|
6
|
+
import { GraphQLDataSourceRequestKind } from '../types';
|
|
5
7
|
|
|
6
8
|
describe('constructing requests', () => {
|
|
7
9
|
it('accepts context', async () => {
|
|
@@ -25,7 +27,7 @@ describe('constructing requests', () => {
|
|
|
25
27
|
name: 'someoneElse',
|
|
26
28
|
},
|
|
27
29
|
];
|
|
28
|
-
return users.find(user => user.id === userId);
|
|
30
|
+
return users.find((user) => user.id === userId);
|
|
29
31
|
},
|
|
30
32
|
},
|
|
31
33
|
};
|
|
@@ -34,9 +36,13 @@ describe('constructing requests', () => {
|
|
|
34
36
|
const DataSource = new LocalGraphQLDataSource(schema);
|
|
35
37
|
|
|
36
38
|
const { data } = await DataSource.process({
|
|
39
|
+
kind: GraphQLDataSourceRequestKind.INCOMING_OPERATION,
|
|
37
40
|
request: {
|
|
38
41
|
query: '{ me { name } }',
|
|
39
42
|
},
|
|
43
|
+
incomingRequestContext: {
|
|
44
|
+
context: { userId: 2 },
|
|
45
|
+
} as GraphQLRequestContext<{userId: number}>,
|
|
40
46
|
context: { userId: 2 },
|
|
41
47
|
});
|
|
42
48
|
|
|
@@ -10,11 +10,20 @@ import {
|
|
|
10
10
|
import { RemoteGraphQLDataSource } from '../RemoteGraphQLDataSource';
|
|
11
11
|
import { Headers } from 'apollo-server-env';
|
|
12
12
|
import { GraphQLRequestContext } from 'apollo-server-types';
|
|
13
|
+
import { GraphQLDataSourceRequestKind } from '../types';
|
|
13
14
|
|
|
14
15
|
beforeEach(() => {
|
|
15
16
|
fetch.mockReset();
|
|
16
17
|
});
|
|
17
18
|
|
|
19
|
+
// Right now, none of these tests care what's on incomingRequestContext, so we
|
|
20
|
+
// pass this fake one in.
|
|
21
|
+
const defaultProcessOptions = {
|
|
22
|
+
kind: GraphQLDataSourceRequestKind.INCOMING_OPERATION,
|
|
23
|
+
incomingRequestContext: {} as any,
|
|
24
|
+
context: {},
|
|
25
|
+
};
|
|
26
|
+
|
|
18
27
|
describe('constructing requests', () => {
|
|
19
28
|
describe('without APQ', () => {
|
|
20
29
|
it('stringifies a request with a query', async () => {
|
|
@@ -26,8 +35,8 @@ describe('constructing requests', () => {
|
|
|
26
35
|
fetch.mockJSONResponseOnce({ data: { me: 'james' } });
|
|
27
36
|
|
|
28
37
|
const { data } = await DataSource.process({
|
|
38
|
+
...defaultProcessOptions,
|
|
29
39
|
request: { query: '{ me { name } }' },
|
|
30
|
-
context: {},
|
|
31
40
|
});
|
|
32
41
|
|
|
33
42
|
expect(data).toEqual({ me: 'james' });
|
|
@@ -46,11 +55,11 @@ describe('constructing requests', () => {
|
|
|
46
55
|
fetch.mockJSONResponseOnce({ data: { me: 'james' } });
|
|
47
56
|
|
|
48
57
|
const { data } = await DataSource.process({
|
|
58
|
+
...defaultProcessOptions,
|
|
49
59
|
request: {
|
|
50
60
|
query: '{ me { name } }',
|
|
51
61
|
variables: { id: '1' },
|
|
52
62
|
},
|
|
53
|
-
context: {},
|
|
54
63
|
});
|
|
55
64
|
|
|
56
65
|
expect(data).toEqual({ me: 'james' });
|
|
@@ -96,8 +105,8 @@ describe('constructing requests', () => {
|
|
|
96
105
|
fetch.mockJSONResponseOnce({ data: { me: 'james' } });
|
|
97
106
|
|
|
98
107
|
const { data } = await DataSource.process({
|
|
108
|
+
...defaultProcessOptions,
|
|
99
109
|
request: { query },
|
|
100
|
-
context: {},
|
|
101
110
|
});
|
|
102
111
|
|
|
103
112
|
expect(data).toEqual({ me: 'james' });
|
|
@@ -135,11 +144,11 @@ describe('constructing requests', () => {
|
|
|
135
144
|
fetch.mockJSONResponseOnce({ data: { me: 'james' } });
|
|
136
145
|
|
|
137
146
|
const { data } = await DataSource.process({
|
|
147
|
+
...defaultProcessOptions,
|
|
138
148
|
request: {
|
|
139
149
|
query,
|
|
140
150
|
variables: { id: '1' },
|
|
141
151
|
},
|
|
142
|
-
context: {},
|
|
143
152
|
});
|
|
144
153
|
|
|
145
154
|
expect(data).toEqual({ me: 'james' });
|
|
@@ -180,8 +189,8 @@ describe('constructing requests', () => {
|
|
|
180
189
|
fetch.mockJSONResponseOnce({ data: { me: 'james' } });
|
|
181
190
|
|
|
182
191
|
const { data } = await DataSource.process({
|
|
192
|
+
...defaultProcessOptions,
|
|
183
193
|
request: { query },
|
|
184
|
-
context: {},
|
|
185
194
|
});
|
|
186
195
|
|
|
187
196
|
expect(data).toEqual({ me: 'james' });
|
|
@@ -207,11 +216,11 @@ describe('constructing requests', () => {
|
|
|
207
216
|
fetch.mockJSONResponseOnce({ data: { me: 'james' } });
|
|
208
217
|
|
|
209
218
|
const { data } = await DataSource.process({
|
|
219
|
+
...defaultProcessOptions,
|
|
210
220
|
request: {
|
|
211
221
|
query,
|
|
212
222
|
variables: { id: '1' },
|
|
213
223
|
},
|
|
214
|
-
context: {},
|
|
215
224
|
});
|
|
216
225
|
|
|
217
226
|
expect(data).toEqual({ me: 'james' });
|
|
@@ -243,11 +252,11 @@ describe('fetcher', () => {
|
|
|
243
252
|
});
|
|
244
253
|
|
|
245
254
|
const { data } = await DataSource.process({
|
|
255
|
+
...defaultProcessOptions,
|
|
246
256
|
request: {
|
|
247
257
|
query: '{ me { name } }',
|
|
248
258
|
variables: { id: '1' },
|
|
249
259
|
},
|
|
250
|
-
context: {},
|
|
251
260
|
});
|
|
252
261
|
|
|
253
262
|
expect(injectedFetch).toHaveBeenCalled();
|
|
@@ -264,11 +273,11 @@ describe('fetcher', () => {
|
|
|
264
273
|
});
|
|
265
274
|
|
|
266
275
|
const { data } = await DataSource.process({
|
|
276
|
+
...defaultProcessOptions,
|
|
267
277
|
request: {
|
|
268
278
|
query: '{ me { name } }',
|
|
269
279
|
variables: { id: '1' },
|
|
270
280
|
},
|
|
271
|
-
context: {},
|
|
272
281
|
});
|
|
273
282
|
|
|
274
283
|
expect(injectedFetch).toHaveBeenCalled();
|
|
@@ -288,11 +297,11 @@ describe('willSendRequest', () => {
|
|
|
288
297
|
fetch.mockJSONResponseOnce({ data: { me: 'james' } });
|
|
289
298
|
|
|
290
299
|
const { data } = await DataSource.process({
|
|
300
|
+
...defaultProcessOptions,
|
|
291
301
|
request: {
|
|
292
302
|
query: '{ me { name } }',
|
|
293
303
|
variables: { id: '1' },
|
|
294
304
|
},
|
|
295
|
-
context: {},
|
|
296
305
|
});
|
|
297
306
|
|
|
298
307
|
expect(data).toEqual({ me: 'james' });
|
|
@@ -307,14 +316,20 @@ describe('willSendRequest', () => {
|
|
|
307
316
|
it('accepts context', async () => {
|
|
308
317
|
const DataSource = new RemoteGraphQLDataSource({
|
|
309
318
|
url: 'https://api.example.com/foo',
|
|
310
|
-
willSendRequest: (
|
|
311
|
-
|
|
319
|
+
willSendRequest: (options) => {
|
|
320
|
+
if (options.kind === GraphQLDataSourceRequestKind.INCOMING_OPERATION) {
|
|
321
|
+
options.request.http?.headers.set(
|
|
322
|
+
'x-user-id',
|
|
323
|
+
options.context.userId,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
312
326
|
},
|
|
313
327
|
});
|
|
314
328
|
|
|
315
329
|
fetch.mockJSONResponseOnce({ data: { me: 'james' } });
|
|
316
330
|
|
|
317
331
|
const { data } = await DataSource.process({
|
|
332
|
+
...defaultProcessOptions,
|
|
318
333
|
request: {
|
|
319
334
|
query: '{ me { name } }',
|
|
320
335
|
variables: { id: '1' },
|
|
@@ -368,6 +383,7 @@ describe('didReceiveResponse', () => {
|
|
|
368
383
|
|
|
369
384
|
const context: MyContext = { surrogateKeys: [] };
|
|
370
385
|
await DataSource.process({
|
|
386
|
+
...defaultProcessOptions,
|
|
371
387
|
request: {
|
|
372
388
|
query: '{ me { name } }',
|
|
373
389
|
variables: { id: '1' },
|
|
@@ -405,11 +421,11 @@ describe('didReceiveResponse', () => {
|
|
|
405
421
|
fetch.mockJSONResponseOnce({ data: { me: 'james' } });
|
|
406
422
|
|
|
407
423
|
await DataSource.process({
|
|
424
|
+
...defaultProcessOptions,
|
|
408
425
|
request: {
|
|
409
426
|
query: '{ me { name } }',
|
|
410
427
|
variables: { id: '1' },
|
|
411
428
|
},
|
|
412
|
-
context: {},
|
|
413
429
|
});
|
|
414
430
|
|
|
415
431
|
expect(spyDidReceiveResponse).toHaveBeenCalledTimes(1);
|
|
@@ -439,11 +455,11 @@ describe('didReceiveResponse', () => {
|
|
|
439
455
|
fetch.mockJSONResponseOnce({ data: { me: 'james' } });
|
|
440
456
|
|
|
441
457
|
await DataSource.process({
|
|
458
|
+
...defaultProcessOptions,
|
|
442
459
|
request: {
|
|
443
460
|
query: '{ me { name } }',
|
|
444
461
|
variables: { id: '1' },
|
|
445
462
|
},
|
|
446
|
-
context: {},
|
|
447
463
|
});
|
|
448
464
|
|
|
449
465
|
expect(spyDidReceiveResponse).toHaveBeenCalledTimes(1);
|
|
@@ -471,15 +487,19 @@ describe('didEncounterError', () => {
|
|
|
471
487
|
|
|
472
488
|
const context: MyContext = { timingData: [] };
|
|
473
489
|
const result = DataSource.process({
|
|
490
|
+
...defaultProcessOptions,
|
|
474
491
|
request: {
|
|
475
492
|
query: '{ me { name } }',
|
|
476
493
|
},
|
|
494
|
+
incomingRequestContext: {
|
|
495
|
+
context,
|
|
496
|
+
} as GraphQLRequestContext<MyContext>,
|
|
477
497
|
context,
|
|
478
498
|
});
|
|
479
499
|
|
|
480
500
|
await expect(result).rejects.toThrow(AuthenticationError);
|
|
481
501
|
expect(context).toMatchObject({
|
|
482
|
-
timingData: [{ time: 1616446845234 }]
|
|
502
|
+
timingData: [{ time: 1616446845234 }],
|
|
483
503
|
});
|
|
484
504
|
});
|
|
485
505
|
});
|
|
@@ -493,8 +513,8 @@ describe('error handling', () => {
|
|
|
493
513
|
fetch.mockResponseOnce('Invalid token', undefined, 401);
|
|
494
514
|
|
|
495
515
|
const result = DataSource.process({
|
|
516
|
+
...defaultProcessOptions,
|
|
496
517
|
request: { query: '{ me { name } }' },
|
|
497
|
-
context: {},
|
|
498
518
|
});
|
|
499
519
|
await expect(result).rejects.toThrow(AuthenticationError);
|
|
500
520
|
await expect(result).rejects.toMatchObject({
|
|
@@ -516,8 +536,8 @@ describe('error handling', () => {
|
|
|
516
536
|
fetch.mockResponseOnce('No access', undefined, 403);
|
|
517
537
|
|
|
518
538
|
const result = DataSource.process({
|
|
539
|
+
...defaultProcessOptions,
|
|
519
540
|
request: { query: '{ me { name } }' },
|
|
520
|
-
context: {},
|
|
521
541
|
});
|
|
522
542
|
await expect(result).rejects.toThrow(ForbiddenError);
|
|
523
543
|
await expect(result).rejects.toMatchObject({
|
|
@@ -539,8 +559,8 @@ describe('error handling', () => {
|
|
|
539
559
|
fetch.mockResponseOnce('Oops', undefined, 500);
|
|
540
560
|
|
|
541
561
|
const result = DataSource.process({
|
|
562
|
+
...defaultProcessOptions,
|
|
542
563
|
request: { query: '{ me { name } }' },
|
|
543
|
-
context: {},
|
|
544
564
|
});
|
|
545
565
|
await expect(result).rejects.toThrow(ApolloError);
|
|
546
566
|
await expect(result).rejects.toMatchObject({
|
|
@@ -571,8 +591,8 @@ describe('error handling', () => {
|
|
|
571
591
|
);
|
|
572
592
|
|
|
573
593
|
const result = DataSource.process({
|
|
594
|
+
...defaultProcessOptions,
|
|
574
595
|
request: { query: '{ me { name } }' },
|
|
575
|
-
context: {},
|
|
576
596
|
});
|
|
577
597
|
await expect(result).rejects.toThrow(ApolloError);
|
|
578
598
|
await expect(result).rejects.toMatchObject({
|
package/src/datasources/index.ts
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Adapted from https://github.com/kornelski/http-cache-semantics
|
|
2
|
+
//
|
|
3
|
+
// Copyright 2016-2018 Kornel Lesiński
|
|
4
|
+
//
|
|
5
|
+
// Redistribution and use in source and binary forms, with or without
|
|
6
|
+
// modification, are permitted provided that the following conditions
|
|
7
|
+
// are met:
|
|
8
|
+
//
|
|
9
|
+
// 1. Redistributions of source code must retain the above copyright
|
|
10
|
+
// notice, this list of conditions and the following disclaimer.
|
|
11
|
+
//
|
|
12
|
+
// 2. Redistributions in binary form must reproduce the above copyright
|
|
13
|
+
// notice, this list of conditions and the following disclaimer in
|
|
14
|
+
// the documentation and/or other materials provided with the
|
|
15
|
+
// distribution.
|
|
16
|
+
//
|
|
17
|
+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
18
|
+
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
19
|
+
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
20
|
+
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
21
|
+
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
22
|
+
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
23
|
+
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
|
24
|
+
// OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
|
25
|
+
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
26
|
+
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
|
|
27
|
+
// WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
28
|
+
// POSSIBILITY OF SUCH DAMAGE.
|
|
29
|
+
|
|
30
|
+
export function parseCacheControlHeader(
|
|
31
|
+
header: string | null | undefined,
|
|
32
|
+
): Record<string, string | true> {
|
|
33
|
+
const cc: Record<string, string | true> = {};
|
|
34
|
+
if (!header) return cc;
|
|
35
|
+
|
|
36
|
+
const parts = header.trim().split(/\s*,\s*/);
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
const [k, v] = part.split(/\s*=\s*/, 2);
|
|
39
|
+
cc[k] = v === undefined ? true : v.replace(/^"|"$/g, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return cc;
|
|
43
|
+
}
|
package/src/datasources/types.ts
CHANGED
|
@@ -1,7 +1,52 @@
|
|
|
1
1
|
import { GraphQLResponse, GraphQLRequestContext } from 'apollo-server-types';
|
|
2
2
|
|
|
3
|
-
export interface GraphQLDataSource<
|
|
3
|
+
export interface GraphQLDataSource<
|
|
4
|
+
TContext extends Record<string, any> = Record<string, any>,
|
|
5
|
+
> {
|
|
4
6
|
process(
|
|
5
|
-
|
|
7
|
+
options: GraphQLDataSourceProcessOptions<TContext>,
|
|
6
8
|
): Promise<GraphQLResponse>;
|
|
7
9
|
}
|
|
10
|
+
|
|
11
|
+
export enum GraphQLDataSourceRequestKind {
|
|
12
|
+
INCOMING_OPERATION = 'incoming operation',
|
|
13
|
+
HEALTH_CHECK = 'health check',
|
|
14
|
+
LOADING_SCHEMA = 'loading schema',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type GraphQLDataSourceProcessOptions<
|
|
18
|
+
TContext extends Record<string, any> = Record<string, any>,
|
|
19
|
+
> = {
|
|
20
|
+
/**
|
|
21
|
+
* The request to send to the subgraph.
|
|
22
|
+
*/
|
|
23
|
+
request: GraphQLRequestContext<TContext>['request'];
|
|
24
|
+
} & (
|
|
25
|
+
| {
|
|
26
|
+
kind: GraphQLDataSourceRequestKind.INCOMING_OPERATION;
|
|
27
|
+
/**
|
|
28
|
+
* The GraphQLRequestContext for the operation received by the gateway, or
|
|
29
|
+
* one of the strings if this operation is generated by the gateway without an
|
|
30
|
+
* incoming request.
|
|
31
|
+
*/
|
|
32
|
+
incomingRequestContext: GraphQLRequestContext<TContext>;
|
|
33
|
+
/**
|
|
34
|
+
* Equivalent to incomingRequestContext.context (provided here for
|
|
35
|
+
* backwards compatibility): the object created by the Apollo Server
|
|
36
|
+
* `context` function.
|
|
37
|
+
*
|
|
38
|
+
* @deprecated Use `incomingRequestContext.context` instead (after
|
|
39
|
+
* checking `kind`).
|
|
40
|
+
*/
|
|
41
|
+
context: GraphQLRequestContext<TContext>['context'];
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
kind:
|
|
45
|
+
| GraphQLDataSourceRequestKind.HEALTH_CHECK
|
|
46
|
+
| GraphQLDataSourceRequestKind.LOADING_SCHEMA;
|
|
47
|
+
/**
|
|
48
|
+
* Mostly provided for historical reasons.
|
|
49
|
+
*/
|
|
50
|
+
context: {};
|
|
51
|
+
}
|
|
52
|
+
);
|
package/src/executeQueryPlan.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from 'graphql';
|
|
16
16
|
import { Trace, google } from 'apollo-reporting-protobuf';
|
|
17
17
|
import { defaultRootOperationNameLookup } from '@apollo/federation';
|
|
18
|
-
import { GraphQLDataSource } from './datasources/types';
|
|
18
|
+
import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types';
|
|
19
19
|
import { OperationContext } from './operationContext';
|
|
20
20
|
import {
|
|
21
21
|
FetchNode,
|
|
@@ -374,11 +374,13 @@ async function executeFetch<TContext>(
|
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
const response = await service.process({
|
|
377
|
+
kind: GraphQLDataSourceRequestKind.INCOMING_OPERATION,
|
|
377
378
|
request: {
|
|
378
379
|
query: source,
|
|
379
380
|
variables,
|
|
380
381
|
http,
|
|
381
382
|
},
|
|
383
|
+
incomingRequestContext: context.requestContext,
|
|
382
384
|
context: context.requestContext.context,
|
|
383
385
|
});
|
|
384
386
|
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from './executeQueryPlan';
|
|
30
30
|
|
|
31
31
|
import { getServiceDefinitionsFromRemoteEndpoint } from './loadServicesFromRemoteEndpoint';
|
|
32
|
-
import { GraphQLDataSource } from './datasources/types';
|
|
32
|
+
import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types';
|
|
33
33
|
import { RemoteGraphQLDataSource } from './datasources/RemoteGraphQLDataSource';
|
|
34
34
|
import { getVariableValues } from 'graphql/execution/values';
|
|
35
35
|
import fetcher from 'make-fetch-happen';
|
|
@@ -699,7 +699,11 @@ export class ApolloGateway implements GraphQLService {
|
|
|
699
699
|
return Promise.all(
|
|
700
700
|
Object.entries(serviceMap).map(([name, { dataSource }]) =>
|
|
701
701
|
dataSource
|
|
702
|
-
.process({
|
|
702
|
+
.process({
|
|
703
|
+
kind: GraphQLDataSourceRequestKind.HEALTH_CHECK,
|
|
704
|
+
request: { query: HEALTH_CHECK_QUERY },
|
|
705
|
+
context: {},
|
|
706
|
+
})
|
|
703
707
|
.then((response) => ({ name, response }))
|
|
704
708
|
.catch((e) => {
|
|
705
709
|
throw new Error(`[${name}]: ${e.message}`);
|
|
@@ -1014,8 +1018,13 @@ export class ApolloGateway implements GraphQLService {
|
|
|
1014
1018
|
public executor = async <TContext>(
|
|
1015
1019
|
requestContext: GraphQLRequestContextExecutionDidStart<TContext>,
|
|
1016
1020
|
): Promise<GraphQLExecutionResult> => {
|
|
1021
|
+
const spanAttributes = requestContext.operationName
|
|
1022
|
+
? { operationName: requestContext.operationName }
|
|
1023
|
+
: {};
|
|
1024
|
+
|
|
1017
1025
|
return tracer.startActiveSpan(
|
|
1018
1026
|
OpenTelemetrySpanNames.REQUEST,
|
|
1027
|
+
{ attributes: spanAttributes },
|
|
1019
1028
|
async (span) => {
|
|
1020
1029
|
try {
|
|
1021
1030
|
const { request, document, queryHash } = requestContext;
|