@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/__generated__/graphqlTypes.d.ts +12 -10
  3. package/dist/__generated__/graphqlTypes.d.ts.map +1 -1
  4. package/dist/datasources/LocalGraphQLDataSource.d.ts +3 -3
  5. package/dist/datasources/LocalGraphQLDataSource.d.ts.map +1 -1
  6. package/dist/datasources/LocalGraphQLDataSource.js +1 -1
  7. package/dist/datasources/LocalGraphQLDataSource.js.map +1 -1
  8. package/dist/datasources/RemoteGraphQLDataSource.d.ts +5 -3
  9. package/dist/datasources/RemoteGraphQLDataSource.d.ts.map +1 -1
  10. package/dist/datasources/RemoteGraphQLDataSource.js +51 -12
  11. package/dist/datasources/RemoteGraphQLDataSource.js.map +1 -1
  12. package/dist/datasources/index.d.ts +1 -1
  13. package/dist/datasources/index.d.ts.map +1 -1
  14. package/dist/datasources/parseCacheControlHeader.d.ts +2 -0
  15. package/dist/datasources/parseCacheControlHeader.d.ts.map +1 -0
  16. package/dist/datasources/parseCacheControlHeader.js +16 -0
  17. package/dist/datasources/parseCacheControlHeader.js.map +1 -0
  18. package/dist/datasources/types.d.ts +16 -1
  19. package/dist/datasources/types.d.ts.map +1 -1
  20. package/dist/datasources/types.js +7 -0
  21. package/dist/datasources/types.js.map +1 -1
  22. package/dist/executeQueryPlan.d.ts.map +1 -1
  23. package/dist/executeQueryPlan.js +3 -0
  24. package/dist/executeQueryPlan.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +10 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/loadServicesFromRemoteEndpoint.d.ts.map +1 -1
  29. package/dist/loadServicesFromRemoteEndpoint.js +9 -4
  30. package/dist/loadServicesFromRemoteEndpoint.js.map +1 -1
  31. package/package.json +8 -8
  32. package/src/__generated__/graphqlTypes.ts +2 -15
  33. package/src/__tests__/gateway/__snapshots__/opentelemetry.test.ts.snap +12 -4
  34. package/src/__tests__/gateway/buildService.test.ts +8 -2
  35. package/src/__tests__/gateway/endToEnd.test.ts +166 -0
  36. package/src/__tests__/gateway/opentelemetry.test.ts +7 -6
  37. package/src/__tests__/gateway/reporting.test.ts +4 -0
  38. package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +5 -0
  39. package/src/datasources/LocalGraphQLDataSource.ts +9 -10
  40. package/src/datasources/RemoteGraphQLDataSource.ts +106 -38
  41. package/src/datasources/__tests__/LocalGraphQLDataSource.test.ts +7 -1
  42. package/src/datasources/__tests__/RemoteGraphQLDataSource.test.ts +38 -18
  43. package/src/datasources/index.ts +1 -1
  44. package/src/datasources/parseCacheControlHeader.ts +43 -0
  45. package/src/datasources/types.ts +47 -2
  46. package/src/executeQueryPlan.ts +3 -1
  47. package/src/index.ts +11 -2
  48. 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<TContext extends Record<string, any> = Record<string, any>> implements GraphQLDataSource<TContext> {
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
- async process({
58
- request,
59
- context,
60
- }: Pick<GraphQLRequestContext<TContext>, 'request' | 'context'>): Promise<
61
- GraphQLResponse
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({ request, context });
89
+ await this.willSendRequest(options);
75
90
  }
76
91
 
77
92
  if (!request.query) {
78
- throw new Error("Missing query");
93
+ throw new Error('Missing query');
79
94
  }
80
95
 
81
96
  const { query, ...requestWithoutQuery } = request;
82
97
 
83
- const respond = (response: GraphQLResponse, request: GraphQLRequest) =>
84
- typeof this.didReceiveResponse === "function"
85
- ? this.didReceiveResponse({ response, request, context })
86
- : response;
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
- await this.sendRequest(requestWithoutQuery, context);
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(error =>
111
- error.message === 'PersistedQueryNotFound')
130
+ !apqOptimisticResponse.errors.find(
131
+ (error) => error.message === 'PersistedQueryNotFound',
132
+ )
112
133
  ) {
113
- return respond(apqOptimisticResponse, requestWithoutQuery);
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(response, requestWithQuery);
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
- requestContext: Pick<
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<Pick<
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: ({ request, context }) => {
311
- request.http?.headers.set('x-user-id', context.userId);
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({
@@ -1,3 +1,3 @@
1
1
  export { LocalGraphQLDataSource } from './LocalGraphQLDataSource';
2
2
  export { RemoteGraphQLDataSource } from './RemoteGraphQLDataSource';
3
- export { GraphQLDataSource } from './types';
3
+ export { GraphQLDataSource, GraphQLDataSourceProcessOptions } from './types';
@@ -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
+ }
@@ -1,7 +1,52 @@
1
1
  import { GraphQLResponse, GraphQLRequestContext } from 'apollo-server-types';
2
2
 
3
- export interface GraphQLDataSource<TContext extends Record<string, any> = Record<string, any>> {
3
+ export interface GraphQLDataSource<
4
+ TContext extends Record<string, any> = Record<string, any>,
5
+ > {
4
6
  process(
5
- request: Pick<GraphQLRequestContext<TContext>, 'request' | 'context'>,
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
+ );
@@ -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({ request: { query: HEALTH_CHECK_QUERY }, context: {} })
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;