@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.
@@ -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 LRUCache from 'lru-cache';
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: LRUCache<string, QueryPlan>;
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 LRUCache<string, QueryPlan>({
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
  }
@@ -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: 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,