@apollo/gateway 0.26.0-alpha.2 → 0.26.3
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 +16 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/datasources/RemoteGraphQLDataSource.d.ts.map +1 -1
- package/dist/datasources/RemoteGraphQLDataSource.js +6 -5
- package/dist/datasources/RemoteGraphQLDataSource.js.map +1 -1
- package/dist/executeQueryPlan.d.ts +1 -1
- package/dist/executeQueryPlan.d.ts.map +1 -1
- package/dist/index.d.ts +4 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -20
- package/dist/index.js.map +1 -1
- package/dist/loadServicesFromRemoteEndpoint.d.ts +8 -8
- package/dist/loadServicesFromRemoteEndpoint.d.ts.map +1 -1
- package/dist/loadServicesFromRemoteEndpoint.js +3 -3
- package/dist/loadServicesFromRemoteEndpoint.js.map +1 -1
- package/dist/operationContext.d.ts +17 -0
- package/dist/operationContext.d.ts.map +1 -0
- package/dist/{buildQueryPlan.js → operationContext.js} +3 -19
- package/dist/operationContext.js.map +1 -0
- package/dist/utilities/assert.d.ts +2 -0
- package/dist/utilities/assert.d.ts.map +1 -0
- package/dist/utilities/assert.js +10 -0
- package/dist/utilities/assert.js.map +1 -0
- package/dist/utilities/graphql.d.ts +0 -1
- package/dist/utilities/graphql.d.ts.map +1 -1
- package/dist/utilities/graphql.js +6 -9
- package/dist/utilities/graphql.js.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/buildQueryPlan.test.ts +28 -76
- package/src/__tests__/executeQueryPlan.test.ts +22 -58
- package/src/__tests__/execution-utils.ts +8 -9
- package/src/__tests__/gateway/queryPlanCache.test.ts +3 -6
- package/src/__tests__/integration/configuration.test.ts +96 -0
- package/src/__tests__/integration/nockMocks.ts +1 -1
- package/src/__tests__/loadServicesFromRemoteEndpoint.test.ts +5 -1
- package/src/__tests__/loadSupergraphSdlFromStorage.test.ts +4 -4
- package/src/__tests__/queryPlanCucumber.test.ts +4 -8
- package/src/config.ts +8 -6
- package/src/datasources/RemoteGraphQLDataSource.ts +7 -6
- package/src/executeQueryPlan.ts +1 -1
- package/src/index.ts +16 -33
- package/src/loadServicesFromRemoteEndpoint.ts +12 -10
- package/src/{buildQueryPlan.ts → operationContext.ts} +6 -35
- package/src/utilities/assert.ts +14 -0
- package/src/utilities/graphql.ts +16 -9
- package/dist/buildQueryPlan.d.ts +0 -17
- package/dist/buildQueryPlan.d.ts.map +0 -1
- package/dist/buildQueryPlan.js.map +0 -1
|
@@ -12,18 +12,17 @@ import {
|
|
|
12
12
|
} from '@apollo/federation';
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
-
buildQueryPlan,
|
|
16
15
|
executeQueryPlan,
|
|
17
16
|
buildOperationContext,
|
|
18
17
|
} from '@apollo/gateway';
|
|
19
|
-
import { QueryPlan } from '@apollo/query-planner';
|
|
18
|
+
import { buildComposedSchema, QueryPlanner, QueryPlan } from '@apollo/query-planner';
|
|
20
19
|
import { LocalGraphQLDataSource } from '../datasources/LocalGraphQLDataSource';
|
|
21
20
|
import { mergeDeep } from 'apollo-utilities';
|
|
22
21
|
|
|
23
22
|
import { queryPlanSerializer, astSerializer } from 'apollo-federation-integration-testsuite';
|
|
24
23
|
import gql from 'graphql-tag';
|
|
25
24
|
import { fixtures } from 'apollo-federation-integration-testsuite';
|
|
26
|
-
import {
|
|
25
|
+
import { parse } from 'graphql';
|
|
27
26
|
|
|
28
27
|
const prettyFormat = require('pretty-format');
|
|
29
28
|
|
|
@@ -56,16 +55,14 @@ export async function execute(
|
|
|
56
55
|
}),
|
|
57
56
|
);
|
|
58
57
|
|
|
59
|
-
const { schema,
|
|
58
|
+
const { schema, queryPlanner } = getFederatedTestingSchema(services);
|
|
60
59
|
|
|
61
60
|
const operationContext = buildOperationContext({
|
|
62
61
|
schema,
|
|
63
62
|
operationDocument: gql`${request.query}`,
|
|
64
|
-
operationString: request.query!,
|
|
65
|
-
queryPlannerPointer,
|
|
66
63
|
});
|
|
67
64
|
|
|
68
|
-
const queryPlan = buildQueryPlan(operationContext);
|
|
65
|
+
const queryPlan = queryPlanner.buildQueryPlan(operationContext);
|
|
69
66
|
|
|
70
67
|
const result = await executeQueryPlan(
|
|
71
68
|
queryPlan,
|
|
@@ -107,9 +104,11 @@ export function getFederatedTestingSchema(services: ServiceDefinitionModule[] =
|
|
|
107
104
|
throw new GraphQLSchemaValidationError(compositionResult.errors);
|
|
108
105
|
}
|
|
109
106
|
|
|
110
|
-
const
|
|
107
|
+
const schema = buildComposedSchema(parse(compositionResult.supergraphSdl))
|
|
111
108
|
|
|
112
|
-
|
|
109
|
+
const queryPlanner = new QueryPlanner(schema);
|
|
110
|
+
|
|
111
|
+
return { serviceMap, schema, queryPlanner };
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
export function getTestingSupergraphSdl(services: typeof fixtures = fixtures) {
|
|
@@ -5,12 +5,9 @@ import { buildFederatedSchema } from '@apollo/federation';
|
|
|
5
5
|
import { LocalGraphQLDataSource } from '../../datasources/LocalGraphQLDataSource';
|
|
6
6
|
import { ApolloGateway } from '../../';
|
|
7
7
|
import { fixtures } from 'apollo-federation-integration-testsuite';
|
|
8
|
-
|
|
8
|
+
import { QueryPlanner } from '@apollo/query-planner';
|
|
9
9
|
it('caches the query plan for a request', async () => {
|
|
10
|
-
const
|
|
11
|
-
const originalPlanner = planner.buildQueryPlan;
|
|
12
|
-
|
|
13
|
-
planner.buildQueryPlan = jest.fn(originalPlanner);
|
|
10
|
+
const buildQueryPlanSpy = jest.spyOn(QueryPlanner.prototype, 'buildQueryPlan');
|
|
14
11
|
|
|
15
12
|
const gateway = new ApolloGateway({
|
|
16
13
|
localServiceList: fixtures,
|
|
@@ -51,7 +48,7 @@ it('caches the query plan for a request', async () => {
|
|
|
51
48
|
});
|
|
52
49
|
|
|
53
50
|
expect(result.data).toEqual(secondResult.data);
|
|
54
|
-
expect(
|
|
51
|
+
expect(buildQueryPlanSpy).toHaveBeenCalledTimes(1);
|
|
55
52
|
});
|
|
56
53
|
|
|
57
54
|
it('supports multiple operations and operationName', async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import gql from 'graphql-tag';
|
|
2
|
+
import http from 'http';
|
|
2
3
|
import mockedEnv from 'mocked-env';
|
|
3
4
|
import { Logger } from 'apollo-server-types';
|
|
4
5
|
import { ApolloGateway } from '../..';
|
|
@@ -204,6 +205,101 @@ describe('gateway config / env behavior', () => {
|
|
|
204
205
|
}
|
|
205
206
|
});
|
|
206
207
|
|
|
208
|
+
describe('introspection headers', () => {
|
|
209
|
+
it('should allow not passing introspectionHeaders', async () => {
|
|
210
|
+
const receivedHeaders = jest.fn();
|
|
211
|
+
const nock = mockSdlQuerySuccess(service);
|
|
212
|
+
nock.on('request', (req: http.ClientRequest) =>
|
|
213
|
+
receivedHeaders(req.getHeaders()),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
gateway = new ApolloGateway({
|
|
217
|
+
serviceList: [{ name: 'accounts', url: service.url }],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await gateway.load(mockApolloConfig);
|
|
221
|
+
|
|
222
|
+
expect(receivedHeaders).toHaveBeenCalledWith(
|
|
223
|
+
expect.objectContaining({
|
|
224
|
+
host: 'localhost:4001',
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should use static headers', async () => {
|
|
230
|
+
const receivedHeaders = jest.fn();
|
|
231
|
+
const nock = mockSdlQuerySuccess(service);
|
|
232
|
+
nock.on('request', (req: http.ClientRequest) =>
|
|
233
|
+
receivedHeaders(req.getHeaders()),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
gateway = new ApolloGateway({
|
|
237
|
+
serviceList: [{ name: 'accounts', url: service.url }],
|
|
238
|
+
introspectionHeaders: {
|
|
239
|
+
Authorization: 'Bearer static',
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await gateway.load(mockApolloConfig);
|
|
244
|
+
|
|
245
|
+
expect(receivedHeaders).toHaveBeenCalledWith(
|
|
246
|
+
expect.objectContaining({
|
|
247
|
+
authorization: ['Bearer static'],
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should use dynamic async headers', async () => {
|
|
253
|
+
const receivedHeaders = jest.fn();
|
|
254
|
+
const nock = mockSdlQuerySuccess(service);
|
|
255
|
+
nock.on('request', (req: http.ClientRequest) =>
|
|
256
|
+
receivedHeaders(req.getHeaders()),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
gateway = new ApolloGateway({
|
|
260
|
+
serviceList: [{ name: 'accounts', url: service.url }],
|
|
261
|
+
introspectionHeaders: async ({ name }) => ({
|
|
262
|
+
Authorization: 'Bearer dynamic-async',
|
|
263
|
+
'X-Service-Name': name,
|
|
264
|
+
}),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await gateway.load(mockApolloConfig);
|
|
268
|
+
|
|
269
|
+
expect(receivedHeaders).toHaveBeenCalledWith(
|
|
270
|
+
expect.objectContaining({
|
|
271
|
+
authorization: ['Bearer dynamic-async'],
|
|
272
|
+
'x-service-name': ['accounts'],
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should use dynamic non-async headers', async () => {
|
|
278
|
+
const receivedHeaders = jest.fn();
|
|
279
|
+
const nock = mockSdlQuerySuccess(service);
|
|
280
|
+
nock.on('request', (req: http.ClientRequest) =>
|
|
281
|
+
receivedHeaders(req.getHeaders()),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
gateway = new ApolloGateway({
|
|
285
|
+
serviceList: [{ name: 'accounts', url: service.url }],
|
|
286
|
+
introspectionHeaders: ({ name }) => ({
|
|
287
|
+
Authorization: 'Bearer dynamic-sync',
|
|
288
|
+
'X-Service-Name': name,
|
|
289
|
+
}),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await gateway.load(mockApolloConfig);
|
|
293
|
+
|
|
294
|
+
expect(receivedHeaders).toHaveBeenCalledWith(
|
|
295
|
+
expect.objectContaining({
|
|
296
|
+
authorization: ['Bearer dynamic-sync'],
|
|
297
|
+
'x-service-name': ['accounts'],
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
207
303
|
// TODO(trevor:cloudconfig): this behavior will be updated
|
|
208
304
|
describe('schema config delivery endpoint configuration', () => {
|
|
209
305
|
it('A code config overrides the env variable', async () => {
|
|
@@ -28,7 +28,7 @@ function mockSdlQuery({ url }: MockService) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function mockSdlQuerySuccess(service: MockService) {
|
|
31
|
-
mockSdlQuery(service).reply(200, {
|
|
31
|
+
return mockSdlQuery(service).reply(200, {
|
|
32
32
|
data: { _service: { sdl: print(service.typeDefs) } },
|
|
33
33
|
});
|
|
34
34
|
}
|
|
@@ -10,6 +10,7 @@ describe('getServiceDefinitionsFromRemoteEndpoint', () => {
|
|
|
10
10
|
getServiceDefinitionsFromRemoteEndpoint({
|
|
11
11
|
serviceList,
|
|
12
12
|
serviceSdlCache,
|
|
13
|
+
getServiceIntrospectionHeaders: async () => ({})
|
|
13
14
|
}),
|
|
14
15
|
).rejects.toThrowError(
|
|
15
16
|
"Tried to load schema for 'test' but no 'url' was specified.",
|
|
@@ -30,7 +31,10 @@ describe('getServiceDefinitionsFromRemoteEndpoint', () => {
|
|
|
30
31
|
getServiceDefinitionsFromRemoteEndpoint({
|
|
31
32
|
serviceList,
|
|
32
33
|
serviceSdlCache,
|
|
34
|
+
getServiceIntrospectionHeaders: async () => ({}),
|
|
33
35
|
}),
|
|
34
|
-
).rejects.toThrowError(
|
|
36
|
+
).rejects.toThrowError(
|
|
37
|
+
/^Couldn't load service definitions for "test" at http:\/\/host-which-better-not-resolve\/graphql: request to http:\/\/host-which-better-not-resolve\/graphql failed, reason: getaddrinfo (ENOTFOUND|EAI_AGAIN)/,
|
|
38
|
+
);
|
|
35
39
|
});
|
|
36
40
|
});
|
|
@@ -74,7 +74,7 @@ describe('loadSupergraphSdlFromStorage', () => {
|
|
|
74
74
|
price: String @join__field(graph: PRODUCT)
|
|
75
75
|
details: ProductDetailsBook @join__field(graph: PRODUCT)
|
|
76
76
|
reviews: [Review] @join__field(graph: REVIEWS)
|
|
77
|
-
relatedReviews: [Review!]! @join__field(graph: REVIEWS, requires: \\"similarBooks
|
|
77
|
+
relatedReviews: [Review!]! @join__field(graph: REVIEWS, requires: \\"similarBooks{isbn}\\")
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
union Brand = Ikea | Amazon
|
|
@@ -250,7 +250,7 @@ describe('loadSupergraphSdlFromStorage', () => {
|
|
|
250
250
|
type User
|
|
251
251
|
@join__owner(graph: ACCOUNTS)
|
|
252
252
|
@join__type(graph: ACCOUNTS, key: \\"id\\")
|
|
253
|
-
@join__type(graph: ACCOUNTS, key: \\"username name
|
|
253
|
+
@join__type(graph: ACCOUNTS, key: \\"username name{first last}\\")
|
|
254
254
|
@join__type(graph: INVENTORY, key: \\"id\\")
|
|
255
255
|
@join__type(graph: PRODUCT, key: \\"id\\")
|
|
256
256
|
@join__type(graph: REVIEWS, key: \\"id\\")
|
|
@@ -261,12 +261,12 @@ describe('loadSupergraphSdlFromStorage', () => {
|
|
|
261
261
|
birthDate(locale: String): String @join__field(graph: ACCOUNTS)
|
|
262
262
|
account: AccountType @join__field(graph: ACCOUNTS)
|
|
263
263
|
metadata: [UserMetadata] @join__field(graph: ACCOUNTS)
|
|
264
|
-
goodDescription: Boolean @join__field(graph: INVENTORY, requires: \\"metadata
|
|
264
|
+
goodDescription: Boolean @join__field(graph: INVENTORY, requires: \\"metadata{description}\\")
|
|
265
265
|
vehicle: Vehicle @join__field(graph: PRODUCT)
|
|
266
266
|
thing: Thing @join__field(graph: PRODUCT)
|
|
267
267
|
reviews: [Review] @join__field(graph: REVIEWS)
|
|
268
268
|
numberOfReviews: Int! @join__field(graph: REVIEWS)
|
|
269
|
-
goodAddress: Boolean @join__field(graph: REVIEWS, requires: \\"metadata
|
|
269
|
+
goodAddress: Boolean @join__field(graph: REVIEWS, requires: \\"metadata{address}\\")
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
type UserMetadata {
|
|
@@ -2,8 +2,8 @@ import gql from 'graphql-tag';
|
|
|
2
2
|
import { defineFeature, loadFeature } from 'jest-cucumber';
|
|
3
3
|
import { DocumentNode } from 'graphql';
|
|
4
4
|
|
|
5
|
-
import { QueryPlan } from '@apollo/query-planner';
|
|
6
|
-
import {
|
|
5
|
+
import { QueryPlan, BuildQueryPlanOptions } from '@apollo/query-planner';
|
|
6
|
+
import { buildOperationContext } from '../operationContext';
|
|
7
7
|
import { getFederatedTestingSchema } from './execution-utils';
|
|
8
8
|
|
|
9
9
|
const buildQueryPlanFeature = loadFeature(
|
|
@@ -20,17 +20,15 @@ features.forEach((feature) => {
|
|
|
20
20
|
feature.scenarios.forEach((scenario) => {
|
|
21
21
|
test(scenario.title, async ({ given, when, then }) => {
|
|
22
22
|
let operationDocument: DocumentNode;
|
|
23
|
-
let operationString: string;
|
|
24
23
|
let queryPlan: QueryPlan;
|
|
25
24
|
let options: BuildQueryPlanOptions = { autoFragmentization: false };
|
|
26
25
|
|
|
27
26
|
// throws on composition errors
|
|
28
|
-
const { schema,
|
|
27
|
+
const { schema, queryPlanner } = getFederatedTestingSchema();
|
|
29
28
|
|
|
30
29
|
const givenQuery = () => {
|
|
31
30
|
given(/^query$/im, (operation: string) => {
|
|
32
31
|
operationDocument = gql(operation);
|
|
33
|
-
operationString = operation;
|
|
34
32
|
})
|
|
35
33
|
}
|
|
36
34
|
|
|
@@ -42,12 +40,10 @@ features.forEach((feature) => {
|
|
|
42
40
|
|
|
43
41
|
const thenQueryPlanShouldBe = () => {
|
|
44
42
|
then(/^query plan$/i, (expectedQueryPlan: string) => {
|
|
45
|
-
queryPlan = buildQueryPlan(
|
|
43
|
+
queryPlan = queryPlanner.buildQueryPlan(
|
|
46
44
|
buildOperationContext({
|
|
47
45
|
schema,
|
|
48
46
|
operationDocument,
|
|
49
|
-
operationString,
|
|
50
|
-
queryPlannerPointer,
|
|
51
47
|
}),
|
|
52
48
|
options
|
|
53
49
|
);
|
package/src/config.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { GraphQLRequestContextExecutionDidStart, Logger } from "apollo-server-ty
|
|
|
5
5
|
import { ServiceDefinition } from "@apollo/federation";
|
|
6
6
|
import { GraphQLDataSource } from './datasources/types';
|
|
7
7
|
import { QueryPlan } from '@apollo/query-planner';
|
|
8
|
-
import { OperationContext } from './';
|
|
8
|
+
import { OperationContext } from './operationContext';
|
|
9
9
|
import { ServiceMap } from './executeQueryPlan';
|
|
10
10
|
|
|
11
11
|
export type ServiceEndpointDefinition = Pick<ServiceDefinition, 'name' | 'url'>;
|
|
@@ -127,17 +127,19 @@ interface GatewayConfigBase {
|
|
|
127
127
|
fetcher?: typeof fetch;
|
|
128
128
|
serviceHealthCheck?: boolean;
|
|
129
129
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
130
|
+
* Enable an upcoming mechanism for fetching a graph’s schema and
|
|
131
|
+
* configuration when using managed federation. This mechanism is currently in
|
|
132
|
+
* a preview state and should not be used in production graphs.
|
|
133
|
+
* See https://go.apollo.dev/g/supergraph-preview for details.
|
|
134
134
|
*/
|
|
135
135
|
experimental_schemaConfigDeliveryEndpoint?: null | string;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
export interface RemoteGatewayConfig extends GatewayConfigBase {
|
|
139
139
|
serviceList: ServiceEndpointDefinition[];
|
|
140
|
-
introspectionHeaders?:
|
|
140
|
+
introspectionHeaders?:
|
|
141
|
+
| HeadersInit
|
|
142
|
+
| ((service: ServiceEndpointDefinition) => Promise<HeadersInit> | HeadersInit);
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
// TODO(trevor:cloudconfig): This type goes away
|
|
@@ -78,10 +78,6 @@ export class RemoteGraphQLDataSource<TContext extends Record<string, any> = Reco
|
|
|
78
78
|
throw new Error("Missing query");
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
const apqHash = createSHA('sha256')
|
|
82
|
-
.update(request.query)
|
|
83
|
-
.digest('hex');
|
|
84
|
-
|
|
85
81
|
const { query, ...requestWithoutQuery } = request;
|
|
86
82
|
|
|
87
83
|
const respond = (response: GraphQLResponse, request: GraphQLRequest) =>
|
|
@@ -90,6 +86,10 @@ export class RemoteGraphQLDataSource<TContext extends Record<string, any> = Reco
|
|
|
90
86
|
: response;
|
|
91
87
|
|
|
92
88
|
if (this.apq) {
|
|
89
|
+
const apqHash = createSHA('sha256')
|
|
90
|
+
.update(request.query)
|
|
91
|
+
.digest('hex');
|
|
92
|
+
|
|
93
93
|
// Take the original extensions and extend them with
|
|
94
94
|
// the necessary "extensions" for APQ handshaking.
|
|
95
95
|
requestWithoutQuery.extensions = {
|
|
@@ -140,9 +140,10 @@ export class RemoteGraphQLDataSource<TContext extends Record<string, any> = Reco
|
|
|
140
140
|
// being transmitted. Instead, we want those to be used to indicate what
|
|
141
141
|
// we're accessing (e.g. url) and what we access it with (e.g. headers).
|
|
142
142
|
const { http, ...requestWithoutHttp } = request;
|
|
143
|
+
const stringifiedRequestWithoutHttp = JSON.stringify(requestWithoutHttp);
|
|
143
144
|
const fetchRequest = new Request(http.url, {
|
|
144
145
|
...http,
|
|
145
|
-
body:
|
|
146
|
+
body: stringifiedRequestWithoutHttp,
|
|
146
147
|
});
|
|
147
148
|
|
|
148
149
|
let fetchResponse: Response | undefined;
|
|
@@ -152,7 +153,7 @@ export class RemoteGraphQLDataSource<TContext extends Record<string, any> = Reco
|
|
|
152
153
|
// Use the fetcher's `Request` implementation for compatibility
|
|
153
154
|
fetchResponse = await this.fetcher(http.url, {
|
|
154
155
|
...http,
|
|
155
|
-
body:
|
|
156
|
+
body: stringifiedRequestWithoutHttp,
|
|
156
157
|
});
|
|
157
158
|
|
|
158
159
|
if (!fetchResponse.ok) {
|
package/src/executeQueryPlan.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { Trace, google } from 'apollo-reporting-protobuf';
|
|
15
15
|
import { defaultRootOperationNameLookup } from '@apollo/federation';
|
|
16
16
|
import { GraphQLDataSource } from './datasources/types';
|
|
17
|
-
import { OperationContext } from './';
|
|
17
|
+
import { OperationContext } from './operationContext';
|
|
18
18
|
import {
|
|
19
19
|
FetchNode,
|
|
20
20
|
PlanNode,
|
package/src/index.ts
CHANGED
|
@@ -18,8 +18,6 @@ import {
|
|
|
18
18
|
VariableDefinitionNode,
|
|
19
19
|
DocumentNode,
|
|
20
20
|
print,
|
|
21
|
-
FragmentDefinitionNode,
|
|
22
|
-
OperationDefinitionNode,
|
|
23
21
|
parse,
|
|
24
22
|
} from 'graphql';
|
|
25
23
|
import {
|
|
@@ -29,7 +27,7 @@ import {
|
|
|
29
27
|
} from '@apollo/federation';
|
|
30
28
|
import loglevel from 'loglevel';
|
|
31
29
|
|
|
32
|
-
import {
|
|
30
|
+
import { buildOperationContext, OperationContext } from './operationContext';
|
|
33
31
|
import {
|
|
34
32
|
executeQueryPlan,
|
|
35
33
|
ServiceMap,
|
|
@@ -43,7 +41,7 @@ import { getVariableValues } from 'graphql/execution/values';
|
|
|
43
41
|
import fetcher from 'make-fetch-happen';
|
|
44
42
|
import { HttpRequestCache } from './cache';
|
|
45
43
|
import { fetch } from 'apollo-server-env';
|
|
46
|
-
import {
|
|
44
|
+
import { QueryPlanner, QueryPlan, prettyFormatQueryPlan } from '@apollo/query-planner';
|
|
47
45
|
import {
|
|
48
46
|
ServiceEndpointDefinition,
|
|
49
47
|
Experimental_DidFailCompositionCallback,
|
|
@@ -73,16 +71,6 @@ import { loadSupergraphSdlFromStorage } from './loadSupergraphSdlFromStorage';
|
|
|
73
71
|
import { getServiceDefinitionsFromStorage } from './legacyLoadServicesFromStorage';
|
|
74
72
|
import { buildComposedSchema } from '@apollo/query-planner';
|
|
75
73
|
|
|
76
|
-
type FragmentMap = { [fragmentName: string]: FragmentDefinitionNode };
|
|
77
|
-
|
|
78
|
-
export type OperationContext = {
|
|
79
|
-
schema: GraphQLSchema;
|
|
80
|
-
operation: OperationDefinitionNode;
|
|
81
|
-
fragments: FragmentMap;
|
|
82
|
-
queryPlannerPointer: QueryPlannerPointer;
|
|
83
|
-
operationString: string;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
74
|
type DataSourceMap = {
|
|
87
75
|
[serviceName: string]: { url?: string; dataSource: GraphQLDataSource };
|
|
88
76
|
};
|
|
@@ -160,7 +148,7 @@ export class ApolloGateway implements GraphQLService {
|
|
|
160
148
|
private compositionMetadata?: CompositionMetadata;
|
|
161
149
|
private serviceSdlCache = new Map<string, string>();
|
|
162
150
|
private warnedStates: WarnedStates = Object.create(null);
|
|
163
|
-
private
|
|
151
|
+
private queryPlanner?: QueryPlanner;
|
|
164
152
|
private parsedSupergraphSdl?: DocumentNode;
|
|
165
153
|
private fetcher: typeof fetch;
|
|
166
154
|
private compositionId?: string;
|
|
@@ -367,7 +355,7 @@ export class ApolloGateway implements GraphQLService {
|
|
|
367
355
|
|
|
368
356
|
this.maybeWarnOnConflictingConfig();
|
|
369
357
|
|
|
370
|
-
// Handles initial assignment of `this.schema`, `this.
|
|
358
|
+
// Handles initial assignment of `this.schema`, `this.queryPlanner`
|
|
371
359
|
isStaticConfig(this.config)
|
|
372
360
|
? this.loadStatic(this.config)
|
|
373
361
|
: await this.loadDynamic(unrefTimer);
|
|
@@ -404,7 +392,7 @@ export class ApolloGateway implements GraphQLService {
|
|
|
404
392
|
this.schema = schema;
|
|
405
393
|
// TODO(trevor): #580 redundant parse
|
|
406
394
|
this.parsedSupergraphSdl = parse(supergraphSdl);
|
|
407
|
-
this.
|
|
395
|
+
this.queryPlanner = new QueryPlanner(schema);
|
|
408
396
|
this.state = { phase: 'loaded' };
|
|
409
397
|
}
|
|
410
398
|
|
|
@@ -482,7 +470,7 @@ export class ApolloGateway implements GraphQLService {
|
|
|
482
470
|
);
|
|
483
471
|
} else {
|
|
484
472
|
this.schema = schema;
|
|
485
|
-
this.
|
|
473
|
+
this.queryPlanner = new QueryPlanner(schema);
|
|
486
474
|
|
|
487
475
|
// Notify the schema listeners of the updated schema
|
|
488
476
|
try {
|
|
@@ -555,7 +543,7 @@ export class ApolloGateway implements GraphQLService {
|
|
|
555
543
|
);
|
|
556
544
|
} else {
|
|
557
545
|
this.schema = schema;
|
|
558
|
-
this.
|
|
546
|
+
this.queryPlanner = new QueryPlanner(schema);
|
|
559
547
|
|
|
560
548
|
// Notify the schema listeners of the updated schema
|
|
561
549
|
try {
|
|
@@ -707,12 +695,7 @@ export class ApolloGateway implements GraphQLService {
|
|
|
707
695
|
throw Error(`Couldn't find graph map in composed schema`);
|
|
708
696
|
}
|
|
709
697
|
|
|
710
|
-
|
|
711
|
-
name: graph.name,
|
|
712
|
-
url: graph.url
|
|
713
|
-
}))
|
|
714
|
-
|
|
715
|
-
return serviceList;
|
|
698
|
+
return Array.from(graphMap.values());
|
|
716
699
|
}
|
|
717
700
|
|
|
718
701
|
private createSchemaFromSupergraphSdl(supergraphSdl: string) {
|
|
@@ -868,9 +851,11 @@ export class ApolloGateway implements GraphQLService {
|
|
|
868
851
|
|
|
869
852
|
return getServiceDefinitionsFromRemoteEndpoint({
|
|
870
853
|
serviceList,
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
854
|
+
async getServiceIntrospectionHeaders(service) {
|
|
855
|
+
return typeof config.introspectionHeaders === 'function'
|
|
856
|
+
? await config.introspectionHeaders(service)
|
|
857
|
+
: config.introspectionHeaders;
|
|
858
|
+
},
|
|
874
859
|
serviceSdlCache: this.serviceSdlCache,
|
|
875
860
|
});
|
|
876
861
|
}
|
|
@@ -943,13 +928,11 @@ export class ApolloGateway implements GraphQLService {
|
|
|
943
928
|
public executor = async <TContext>(
|
|
944
929
|
requestContext: GraphQLRequestContextExecutionDidStart<TContext>,
|
|
945
930
|
): Promise<GraphQLExecutionResult> => {
|
|
946
|
-
const { request, document, queryHash
|
|
931
|
+
const { request, document, queryHash } = requestContext;
|
|
947
932
|
const queryPlanStoreKey = queryHash + (request.operationName || '');
|
|
948
933
|
const operationContext = buildOperationContext({
|
|
949
934
|
schema: this.schema!,
|
|
950
935
|
operationDocument: document,
|
|
951
|
-
operationString: source,
|
|
952
|
-
queryPlannerPointer: this.queryPlannerPointer!,
|
|
953
936
|
operationName: request.operationName,
|
|
954
937
|
});
|
|
955
938
|
|
|
@@ -970,7 +953,8 @@ export class ApolloGateway implements GraphQLService {
|
|
|
970
953
|
}
|
|
971
954
|
|
|
972
955
|
if (!queryPlan) {
|
|
973
|
-
|
|
956
|
+
// TODO(#631): Can we be sure the query planner has been initialized here?
|
|
957
|
+
queryPlan = this.queryPlanner!.buildQueryPlan(operationContext, {
|
|
974
958
|
autoFragmentization: Boolean(
|
|
975
959
|
this.config.experimental_autoFragmentization,
|
|
976
960
|
),
|
|
@@ -1175,7 +1159,6 @@ class UnreachableCaseError extends Error {
|
|
|
1175
1159
|
}
|
|
1176
1160
|
|
|
1177
1161
|
export {
|
|
1178
|
-
buildQueryPlan,
|
|
1179
1162
|
executeQueryPlan,
|
|
1180
1163
|
buildOperationContext,
|
|
1181
1164
|
ServiceMap,
|
|
@@ -3,20 +3,22 @@ import { parse } from 'graphql';
|
|
|
3
3
|
import { Headers, HeadersInit } from 'node-fetch';
|
|
4
4
|
import { GraphQLDataSource } from './datasources/types';
|
|
5
5
|
import { SERVICE_DEFINITION_QUERY } from './';
|
|
6
|
-
import { CompositionUpdate } from './config';
|
|
6
|
+
import { CompositionUpdate, ServiceEndpointDefinition } from './config';
|
|
7
7
|
import { ServiceDefinition } from '@apollo/federation';
|
|
8
8
|
|
|
9
|
+
type Service = ServiceEndpointDefinition & {
|
|
10
|
+
dataSource: GraphQLDataSource;
|
|
11
|
+
};
|
|
12
|
+
|
|
9
13
|
export async function getServiceDefinitionsFromRemoteEndpoint({
|
|
10
14
|
serviceList,
|
|
11
|
-
|
|
15
|
+
getServiceIntrospectionHeaders,
|
|
12
16
|
serviceSdlCache,
|
|
13
17
|
}: {
|
|
14
|
-
serviceList:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}[];
|
|
19
|
-
headers?: HeadersInit;
|
|
18
|
+
serviceList: Service[];
|
|
19
|
+
getServiceIntrospectionHeaders: (
|
|
20
|
+
service: ServiceEndpointDefinition,
|
|
21
|
+
) => Promise<HeadersInit | undefined>;
|
|
20
22
|
serviceSdlCache: Map<string, string>;
|
|
21
23
|
}): Promise<CompositionUpdate> {
|
|
22
24
|
if (!serviceList || !serviceList.length) {
|
|
@@ -27,7 +29,7 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
|
|
|
27
29
|
|
|
28
30
|
let isNewSchema = false;
|
|
29
31
|
// for each service, fetch its introspection schema
|
|
30
|
-
const promiseOfServiceList = serviceList.map(({ name, url, dataSource }) => {
|
|
32
|
+
const promiseOfServiceList = serviceList.map(async ({ name, url, dataSource }) => {
|
|
31
33
|
if (!url) {
|
|
32
34
|
throw new Error(
|
|
33
35
|
`Tried to load schema for '${name}' but no 'url' was specified.`);
|
|
@@ -38,7 +40,7 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
|
|
|
38
40
|
http: {
|
|
39
41
|
url,
|
|
40
42
|
method: 'POST',
|
|
41
|
-
headers: new Headers(
|
|
43
|
+
headers: new Headers(await getServiceIntrospectionHeaders({ name, url })),
|
|
42
44
|
},
|
|
43
45
|
};
|
|
44
46
|
|
|
@@ -5,41 +5,26 @@ import {
|
|
|
5
5
|
GraphQLSchema,
|
|
6
6
|
Kind,
|
|
7
7
|
OperationDefinitionNode,
|
|
8
|
-
print,
|
|
9
8
|
} from 'graphql';
|
|
10
|
-
import { OperationContext } from './';
|
|
11
|
-
import { getQueryPlan, QueryPlan, QueryPlannerPointer } from '@apollo/query-planner';
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
autoFragmentization: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function buildQueryPlan(
|
|
18
|
-
operationContext: OperationContext,
|
|
19
|
-
options: BuildQueryPlanOptions = { autoFragmentization: false },
|
|
20
|
-
): QueryPlan {
|
|
10
|
+
type FragmentMap = { [fragmentName: string]: FragmentDefinitionNode };
|
|
21
11
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
12
|
+
export type OperationContext = {
|
|
13
|
+
schema: GraphQLSchema;
|
|
14
|
+
operation: OperationDefinitionNode;
|
|
15
|
+
fragments: FragmentMap;
|
|
16
|
+
};
|
|
28
17
|
|
|
29
18
|
// Adapted from buildExecutionContext in graphql-js
|
|
30
19
|
interface BuildOperationContextOptions {
|
|
31
20
|
schema: GraphQLSchema;
|
|
32
21
|
operationDocument: DocumentNode;
|
|
33
|
-
operationString: string;
|
|
34
|
-
queryPlannerPointer: QueryPlannerPointer;
|
|
35
22
|
operationName?: string;
|
|
36
23
|
};
|
|
37
24
|
|
|
38
25
|
export function buildOperationContext({
|
|
39
26
|
schema,
|
|
40
27
|
operationDocument,
|
|
41
|
-
operationString,
|
|
42
|
-
queryPlannerPointer,
|
|
43
28
|
operationName,
|
|
44
29
|
}: BuildOperationContextOptions): OperationContext {
|
|
45
30
|
let operation: OperationDefinitionNode | undefined;
|
|
@@ -77,23 +62,9 @@ export function buildOperationContext({
|
|
|
77
62
|
}
|
|
78
63
|
}
|
|
79
64
|
|
|
80
|
-
// In the case of multiple operations specified (operationName presence validated above),
|
|
81
|
-
// `operation` === the operation specified by `operationName`
|
|
82
|
-
const trimmedOperationString = operationCount > 1
|
|
83
|
-
? print({
|
|
84
|
-
kind: Kind.DOCUMENT,
|
|
85
|
-
definitions: [
|
|
86
|
-
operation,
|
|
87
|
-
...Object.values(fragments),
|
|
88
|
-
],
|
|
89
|
-
})
|
|
90
|
-
: operationString;
|
|
91
|
-
|
|
92
65
|
return {
|
|
93
66
|
schema,
|
|
94
67
|
operation,
|
|
95
68
|
fragments,
|
|
96
|
-
queryPlannerPointer,
|
|
97
|
-
operationString: trimmedOperationString
|
|
98
69
|
};
|
|
99
70
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For lack of a "home of federation utilities", this function is copy/pasted
|
|
3
|
+
* verbatim across the federation, gateway, and query-planner packages. Any changes
|
|
4
|
+
* made here should be reflected in the other two locations as well.
|
|
5
|
+
*
|
|
6
|
+
* @param condition
|
|
7
|
+
* @param message
|
|
8
|
+
* @throws {@link Error}
|
|
9
|
+
*/
|
|
10
|
+
export function assert(condition: any, message: string): asserts condition {
|
|
11
|
+
if (!condition) {
|
|
12
|
+
throw new Error(message);
|
|
13
|
+
}
|
|
14
|
+
}
|