@apollo/gateway 2.3.3 → 2.4.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/dist/executeQueryPlan.d.ts.map +1 -1
- package/dist/executeQueryPlan.js +38 -12
- package/dist/executeQueryPlan.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -9
- package/dist/index.js.map +1 -1
- package/dist/resultShaping.d.ts.map +1 -1
- package/dist/resultShaping.js +4 -2
- package/dist/resultShaping.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/executeQueryPlan.conditions.test.ts +1488 -0
- package/src/__tests__/executeQueryPlan.test.ts +28 -16
- package/src/__tests__/execution-utils.ts +3 -3
- package/src/__tests__/gateway/buildService.test.ts +17 -13
- package/src/__tests__/gateway/endToEnd.test.ts +91 -94
- package/src/__tests__/gateway/queryPlanCache.test.ts +18 -19
- package/src/__tests__/gateway/queryPlannerConfig.test.ts +101 -0
- package/src/__tests__/gateway/reporting.test.ts +23 -45
- package/src/__tests__/gateway/supergraphSdl.test.ts +5 -3
- package/src/__tests__/gateway/testUtils.ts +89 -0
- package/src/__tests__/integration/aliases.test.ts +5 -5
- package/src/__tests__/integration/boolean.test.ts +9 -9
- package/src/__tests__/integration/managed.test.ts +10 -9
- package/src/__tests__/resultShaping.test.ts +75 -3
- package/src/executeQueryPlan.ts +58 -18
- package/src/index.ts +10 -5
- package/src/resultShaping.ts +8 -4
package/src/executeQueryPlan.ts
CHANGED
|
@@ -28,12 +28,13 @@ import {
|
|
|
28
28
|
getResponseName,
|
|
29
29
|
FetchDataInputRewrite,
|
|
30
30
|
FetchDataOutputRewrite,
|
|
31
|
+
evaluateCondition,
|
|
31
32
|
} from '@apollo/query-planner';
|
|
32
33
|
import { deepMerge } from './utilities/deepMerge';
|
|
33
34
|
import { isNotNullOrUndefined } from './utilities/array';
|
|
34
35
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
35
36
|
import { OpenTelemetrySpanNames, tracer } from "./utilities/opentelemetry";
|
|
36
|
-
import { assert, defaultRootName, errorCodeDef, ERRORS, isDefined, operationFromDocument, Schema } from '@apollo/federation-internals';
|
|
37
|
+
import { assert, defaultRootName, errorCodeDef, ERRORS, isDefined, Operation, operationFromDocument, Schema } from '@apollo/federation-internals';
|
|
37
38
|
import { GatewayGraphQLRequestContext, GatewayExecutionResult } from '@apollo/server-gateway-interface';
|
|
38
39
|
import { computeResponse } from './resultShaping';
|
|
39
40
|
|
|
@@ -64,6 +65,7 @@ type ResultCursor = {
|
|
|
64
65
|
interface ExecutionContext {
|
|
65
66
|
queryPlan: QueryPlan;
|
|
66
67
|
operationContext: OperationContext;
|
|
68
|
+
operation: Operation,
|
|
67
69
|
serviceMap: ServiceMap;
|
|
68
70
|
requestContext: GatewayGraphQLRequestContext;
|
|
69
71
|
supergraphSchema: GraphQLSchema;
|
|
@@ -114,9 +116,39 @@ export async function executeQueryPlan(
|
|
|
114
116
|
try {
|
|
115
117
|
const errors: GraphQLError[] = [];
|
|
116
118
|
|
|
119
|
+
let operation: Operation;
|
|
120
|
+
try {
|
|
121
|
+
operation = operationFromDocument(
|
|
122
|
+
apiSchema,
|
|
123
|
+
{
|
|
124
|
+
kind: Kind.DOCUMENT,
|
|
125
|
+
definitions: [
|
|
126
|
+
operationContext.operation,
|
|
127
|
+
...Object.values(operationContext.fragments),
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
validate: false,
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
// We shouldn't really have errors as the operation should already have been validated, but if something still
|
|
136
|
+
// happens, we should report it properly (plus, some of our tests call this method directly and blow up if we don't
|
|
137
|
+
// handle this correctly).
|
|
138
|
+
// TODO: we are doing some duplicate work by building both `OperationContext` and this `Operation`. Ideally we
|
|
139
|
+
// would remove `OperationContext`, pass the `Operation` directly to this method, and only use that. This would change
|
|
140
|
+
// the signature of this method though and it is exported so ... maybe later ?
|
|
141
|
+
//
|
|
142
|
+
if (err instanceof GraphQLError) {
|
|
143
|
+
return { errors: [err] };
|
|
144
|
+
}
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
|
|
117
148
|
const context: ExecutionContext = {
|
|
118
149
|
queryPlan,
|
|
119
150
|
operationContext,
|
|
151
|
+
operation,
|
|
120
152
|
serviceMap,
|
|
121
153
|
requestContext,
|
|
122
154
|
supergraphSchema,
|
|
@@ -129,6 +161,10 @@ export async function executeQueryPlan(
|
|
|
129
161
|
requestContext.metrics && requestContext.metrics.captureTraces
|
|
130
162
|
);
|
|
131
163
|
|
|
164
|
+
if (queryPlan.node?.kind === 'Subscription') {
|
|
165
|
+
throw new Error('Execution of subscriptions not supported by gateway');
|
|
166
|
+
}
|
|
167
|
+
|
|
132
168
|
if (queryPlan.node) {
|
|
133
169
|
const traceNode = await executeNode(
|
|
134
170
|
context,
|
|
@@ -148,20 +184,6 @@ export async function executeQueryPlan(
|
|
|
148
184
|
const result = await tracer.startActiveSpan(OpenTelemetrySpanNames.POST_PROCESSING, async (span) => {
|
|
149
185
|
let data;
|
|
150
186
|
try {
|
|
151
|
-
const operation = operationFromDocument(
|
|
152
|
-
apiSchema,
|
|
153
|
-
{
|
|
154
|
-
kind: Kind.DOCUMENT,
|
|
155
|
-
definitions: [
|
|
156
|
-
operationContext.operation,
|
|
157
|
-
...Object.values(operationContext.fragments),
|
|
158
|
-
],
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
validate: false,
|
|
162
|
-
}
|
|
163
|
-
);
|
|
164
|
-
|
|
165
187
|
let postProcessingErrors: GraphQLError[];
|
|
166
188
|
({ data, errors: postProcessingErrors } = computeResponse({
|
|
167
189
|
operation,
|
|
@@ -326,12 +348,30 @@ async function executeNode(
|
|
|
326
348
|
}
|
|
327
349
|
return new Trace.QueryPlanNode({ fetch: traceNode });
|
|
328
350
|
}
|
|
351
|
+
case 'Condition': {
|
|
352
|
+
const condition = evaluateCondition(node, context.operation.variableDefinitions, context.requestContext.request.variables);
|
|
353
|
+
const pickedBranch = condition ? node.ifClause : node.elseClause;
|
|
354
|
+
let branchTraceNode: Trace.QueryPlanNode | undefined = undefined;
|
|
355
|
+
if (pickedBranch) {
|
|
356
|
+
branchTraceNode = await executeNode(
|
|
357
|
+
context,
|
|
358
|
+
pickedBranch,
|
|
359
|
+
currentCursor,
|
|
360
|
+
captureTraces
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return new Trace.QueryPlanNode({
|
|
365
|
+
condition: new Trace.QueryPlanNode.ConditionNode({
|
|
366
|
+
condition: node.condition,
|
|
367
|
+
ifClause: condition ? branchTraceNode : undefined,
|
|
368
|
+
elseClause: condition ? undefined : branchTraceNode,
|
|
369
|
+
}),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
329
372
|
case 'Defer': {
|
|
330
373
|
assert(false, `@defer support is not available in the gateway`);
|
|
331
374
|
}
|
|
332
|
-
case 'Condition': {
|
|
333
|
-
assert(false, `Condition nodes are not available in the gateway`);
|
|
334
|
-
}
|
|
335
375
|
}
|
|
336
376
|
}
|
|
337
377
|
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { deprecate } from 'util';
|
|
2
2
|
import { createHash } from '@apollo/utils.createhash';
|
|
3
3
|
import type { Logger } from '@apollo/utils.logger';
|
|
4
|
-
import
|
|
4
|
+
import { QueryPlanCache } from '@apollo/query-planner'
|
|
5
|
+
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
|
|
5
6
|
import {
|
|
6
7
|
GraphQLSchema,
|
|
7
8
|
VariableDefinitionNode
|
|
@@ -123,7 +124,7 @@ export class ApolloGateway implements GatewayInterface {
|
|
|
123
124
|
private serviceMap: DataSourceMap = Object.create(null);
|
|
124
125
|
private config: GatewayConfig;
|
|
125
126
|
private logger: Logger;
|
|
126
|
-
private queryPlanStore:
|
|
127
|
+
private queryPlanStore: QueryPlanCache;
|
|
127
128
|
private apolloConfig?: ApolloConfigFromAS3;
|
|
128
129
|
private onSchemaChangeListeners = new Set<(schema: GraphQLSchema) => void>();
|
|
129
130
|
private onSchemaLoadOrUpdateListeners = new Set<
|
|
@@ -189,6 +190,9 @@ export class ApolloGateway implements GatewayInterface {
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
private initQueryPlanStore(approximateQueryPlanStoreMiB?: number) {
|
|
193
|
+
if(this.config.queryPlannerConfig?.cache){
|
|
194
|
+
return this.config.queryPlannerConfig?.cache
|
|
195
|
+
}
|
|
192
196
|
// Create ~about~ a 30MiB InMemoryLRUCache (or 50MiB if the full operation ASTs are
|
|
193
197
|
// enabled in query plans as this requires plans to use more memory). This is
|
|
194
198
|
// less than precise since the technique to calculate the size of a DocumentNode is
|
|
@@ -196,7 +200,7 @@ export class ApolloGateway implements GatewayInterface {
|
|
|
196
200
|
// for unicode characters, etc.), but it should do a reasonable job at
|
|
197
201
|
// providing a caching document store for most operations.
|
|
198
202
|
const defaultSize = this.config.queryPlannerConfig?.exposeDocumentNodeInFetchNode ? 50 : 30;
|
|
199
|
-
return new
|
|
203
|
+
return new InMemoryLRUCache<QueryPlan>({
|
|
200
204
|
maxSize: Math.pow(2, 20) * (approximateQueryPlanStoreMiB || defaultSize),
|
|
201
205
|
sizeCalculation: approximateObjectSize,
|
|
202
206
|
});
|
|
@@ -770,7 +774,7 @@ export class ApolloGateway implements GatewayInterface {
|
|
|
770
774
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
771
775
|
return { errors: validationErrors };
|
|
772
776
|
}
|
|
773
|
-
let queryPlan = this.queryPlanStore.get(queryPlanStoreKey);
|
|
777
|
+
let queryPlan = await this.queryPlanStore.get(queryPlanStoreKey);
|
|
774
778
|
|
|
775
779
|
if (!queryPlan) {
|
|
776
780
|
queryPlan = tracer.startActiveSpan(
|
|
@@ -794,7 +798,7 @@ export class ApolloGateway implements GatewayInterface {
|
|
|
794
798
|
);
|
|
795
799
|
|
|
796
800
|
try {
|
|
797
|
-
this.queryPlanStore.set(queryPlanStoreKey, queryPlan);
|
|
801
|
+
await this.queryPlanStore.set(queryPlanStoreKey, queryPlan);
|
|
798
802
|
} catch (err) {
|
|
799
803
|
this.logger.warn(
|
|
800
804
|
'Could not store queryPlan' + ((err && err.message) || err),
|
|
@@ -969,6 +973,7 @@ export class ApolloGateway implements GatewayInterface {
|
|
|
969
973
|
state: this.state,
|
|
970
974
|
compositionId: this.compositionId,
|
|
971
975
|
supergraphSdl: this.supergraphSdl,
|
|
976
|
+
queryPlanner: this.queryPlanner,
|
|
972
977
|
};
|
|
973
978
|
}
|
|
974
979
|
}
|
package/src/resultShaping.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { GraphQLError } from "graphql";
|
|
|
26
26
|
* Performs post-query plan execution processing of internally fetched data to produce the final query response.
|
|
27
27
|
*
|
|
28
28
|
* The reason for this post-processing are the following ones:
|
|
29
|
-
* 1. executing the query plan will usually query more fields that are strictly requested. That is because key, required
|
|
29
|
+
* 1. executing the query plan will usually query more fields that are strictly requested. That is because key, required
|
|
30
30
|
* and __typename fields must often be requested to subgraphs even when they are not part of the query. So this method
|
|
31
31
|
* will filter out anything that has been fetched but isn't part of the user query.
|
|
32
32
|
* 2. query plan execution does not guarantee that in the data fetched, the fields will respect the ordering that the
|
|
@@ -60,7 +60,7 @@ export function computeResponse({
|
|
|
60
60
|
variables?: Record<string, any>,
|
|
61
61
|
input: Record<string, any> | null | undefined,
|
|
62
62
|
introspectionHandling: (introspectionSelection: FieldSelection) => any,
|
|
63
|
-
}): {
|
|
63
|
+
}): {
|
|
64
64
|
data: Record<string, any> | null | undefined,
|
|
65
65
|
errors: GraphQLError[],
|
|
66
66
|
} {
|
|
@@ -70,7 +70,11 @@ export function computeResponse({
|
|
|
70
70
|
|
|
71
71
|
const parameters = {
|
|
72
72
|
schema: operation.schema,
|
|
73
|
-
variables:
|
|
73
|
+
variables: {
|
|
74
|
+
...operation.collectDefaultedVariableValues(),
|
|
75
|
+
// overwrite any defaulted variables if they are provided
|
|
76
|
+
...variables,
|
|
77
|
+
},
|
|
74
78
|
errors: [],
|
|
75
79
|
introspectionHandling,
|
|
76
80
|
};
|
|
@@ -119,7 +123,7 @@ function ifValue(directive: Directive<any, { if: boolean | Variable }>, variable
|
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
enum ApplyResult { OK, NULL_BUBBLE_UP }
|
|
126
|
+
enum ApplyResult { OK, NULL_BUBBLE_UP }
|
|
123
127
|
|
|
124
128
|
function typeConditionApplies(
|
|
125
129
|
schema: Schema,
|