@apollo/gateway 2.3.4 → 2.3.6

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.
@@ -0,0 +1,130 @@
1
+ import { FetchDataRewrite } from "@apollo/query-planner";
2
+ import { assert } from "console";
3
+ import { GraphQLSchema, isAbstractType, isInterfaceType, isObjectType } from "graphql";
4
+
5
+ const FRAGMENT_PREFIX = '... on ';
6
+
7
+ export function applyRewrites(schema: GraphQLSchema, rewrites: FetchDataRewrite[] | undefined, value: Record<string, any>) {
8
+ if (!rewrites) {
9
+ return;
10
+ }
11
+
12
+ for (const rewrite of rewrites) {
13
+ applyRewrite(schema, rewrite, value);
14
+ }
15
+ }
16
+
17
+ function applyRewrite(schema: GraphQLSchema, rewrite: FetchDataRewrite, value: Record<string, any>) {
18
+ const splitted = splitPathLastElement(rewrite.path);
19
+ if (!splitted) {
20
+ return;
21
+ }
22
+
23
+ const [parent, last] = splitted;
24
+ const { kind, value: fieldName } = parsePathElement(last);
25
+ // So far, all rewrites finish by a field name. If this ever changes, this assertion will catch it early and we can update.
26
+ assert(kind === 'fieldName', () => `Unexpected fragment as last element of ${rewrite.path}`);
27
+ applyAtPath(schema, parent, value, rewriteAtPathFunction(rewrite, fieldName));
28
+ }
29
+
30
+ function rewriteAtPathFunction(rewrite: FetchDataRewrite, fieldAtPath: string): (obj: Record<string, any>) => void {
31
+ switch (rewrite.kind) {
32
+ case 'ValueSetter':
33
+ return (obj) => {
34
+ obj[fieldAtPath] = rewrite.setValueTo;
35
+ };
36
+ case 'KeyRenamer':
37
+ return (obj) => {
38
+ obj[rewrite.renameKeyTo] = obj[fieldAtPath];
39
+ obj[fieldAtPath] = undefined;
40
+ };
41
+ }
42
+ }
43
+
44
+
45
+ /**
46
+ * Given a path, separates the last element of path and the rest of it and return them as a pair.
47
+ * This will return `undefined` if the path is empty.
48
+ */
49
+ function splitPathLastElement(path: string[]): [string[], string] | undefined {
50
+ if (path.length === 0) {
51
+ return undefined;
52
+ }
53
+
54
+ const lastIdx = path.length - 1;
55
+ return [path.slice(0, lastIdx), path[lastIdx]];
56
+ }
57
+
58
+ function applyAtPath(schema: GraphQLSchema, path: string[], value: any, fct: (objAtPath: Record<string, any>) => void) {
59
+ if (Array.isArray(value)) {
60
+ for (const arrayValue of value) {
61
+ applyAtPath(schema, path, arrayValue, fct);
62
+ }
63
+ return;
64
+ }
65
+
66
+ if (typeof value !== 'object') {
67
+ return;
68
+ }
69
+
70
+ if (path.length === 0) {
71
+ fct(value);
72
+ return;
73
+ }
74
+
75
+ const [first, ...rest] = path;
76
+ const { kind, value: eltValue } = parsePathElement(first);
77
+ switch (kind) {
78
+ case 'fieldName':
79
+ applyAtPath(schema, rest, value[eltValue], fct);
80
+ break;
81
+ case 'typeName':
82
+ // When we apply rewrites, we don't always have the __typename of all object we would need to, but the code expects that
83
+ // this does not stop the rewrite to applying, hence the modified to `true` when the object typename is not found.
84
+ if (isObjectOfType(schema, value, eltValue, true)) {
85
+ applyAtPath(schema, rest, value, fct);
86
+ }
87
+ break;
88
+ }
89
+ }
90
+
91
+ function parsePathElement(elt: string): { kind: 'fieldName' | 'typeName', value: string } {
92
+ if (elt.startsWith(FRAGMENT_PREFIX)) {
93
+ return { kind: 'typeName', value: elt.slice(FRAGMENT_PREFIX.length) };
94
+ } else {
95
+ return { kind: 'fieldName', value: elt };
96
+ }
97
+ }
98
+
99
+
100
+ export function isObjectOfType(
101
+ schema: GraphQLSchema,
102
+ obj: Record<string, any>,
103
+ typeCondition: string,
104
+ defaultOnUnknownObjectType: boolean = false,
105
+ ): boolean {
106
+ const objTypename = obj['__typename'];
107
+ if (!objTypename) {
108
+ return defaultOnUnknownObjectType;
109
+ }
110
+
111
+ if (typeCondition === objTypename) {
112
+ return true;
113
+ }
114
+
115
+ const type = schema.getType(objTypename);
116
+ if (!type) {
117
+ return false;
118
+ }
119
+
120
+ const conditionalType = schema.getType(typeCondition);
121
+ if (!conditionalType) {
122
+ return false;
123
+ }
124
+
125
+ if (isAbstractType(conditionalType)) {
126
+ return (isObjectType(type) || isInterfaceType(type)) && schema.isSubType(conditionalType, type);
127
+ }
128
+
129
+ return false;
130
+ }
@@ -5,15 +5,15 @@ import {
5
5
  TypeNameMetaFieldDef,
6
6
  GraphQLFieldResolver,
7
7
  GraphQLFormattedError,
8
- isAbstractType,
9
8
  GraphQLSchema,
10
- isObjectType,
11
- isInterfaceType,
12
9
  GraphQLErrorOptions,
13
10
  DocumentNode,
14
11
  executeSync,
15
12
  OperationTypeNode,
16
13
  FieldNode,
14
+ visit,
15
+ ASTNode,
16
+ VariableDefinitionNode,
17
17
  } from 'graphql';
18
18
  import { Trace, google } from '@apollo/usage-reporting-protobuf';
19
19
  import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types';
@@ -26,16 +26,15 @@ import {
26
26
  QueryPlanSelectionNode,
27
27
  QueryPlanFieldNode,
28
28
  getResponseName,
29
- FetchDataInputRewrite,
30
- FetchDataOutputRewrite,
31
29
  } from '@apollo/query-planner';
32
30
  import { deepMerge } from './utilities/deepMerge';
33
31
  import { isNotNullOrUndefined } from './utilities/array';
34
32
  import { SpanStatusCode } from "@opentelemetry/api";
35
33
  import { OpenTelemetrySpanNames, tracer } from "./utilities/opentelemetry";
36
- import { assert, defaultRootName, errorCodeDef, ERRORS, isDefined, operationFromDocument, Schema } from '@apollo/federation-internals';
34
+ import { assert, defaultRootName, errorCodeDef, ERRORS, operationFromDocument, Schema } from '@apollo/federation-internals';
37
35
  import { GatewayGraphQLRequestContext, GatewayExecutionResult } from '@apollo/server-gateway-interface';
38
36
  import { computeResponse } from './resultShaping';
37
+ import { applyRewrites, isObjectOfType } from './dataRewrites';
39
38
 
40
39
  export type ServiceMap = {
41
40
  [serviceName: string]: GraphQLDataSource;
@@ -70,13 +69,33 @@ interface ExecutionContext {
70
69
  errors: GraphQLError[];
71
70
  }
72
71
 
73
- function makeIntrospectionQueryDocument(introspectionSelection: FieldNode): DocumentNode {
72
+ function collectUsedVariables(node: ASTNode): Set<string> {
73
+ const usedVariables = new Set<string>();
74
+ visit(node, {
75
+ Variable: ({ name }) => {
76
+ usedVariables.add(name.value);
77
+ }
78
+ });
79
+ return usedVariables;
80
+ }
81
+
82
+ function makeIntrospectionQueryDocument(
83
+ introspectionSelection: FieldNode,
84
+ variableDefinitions?: readonly VariableDefinitionNode[],
85
+ ): DocumentNode {
86
+ const usedVariables = collectUsedVariables(introspectionSelection);
87
+ const usedVariableDefinitions = variableDefinitions?.filter((def) => usedVariables.has(def.variable.name.value));
88
+ assert(
89
+ usedVariables.size === (usedVariableDefinitions?.length ?? 0),
90
+ () => `Should have found all used variables ${[...usedVariables]} in definitions ${JSON.stringify(variableDefinitions)}`,
91
+ );
74
92
  return {
75
93
  kind: Kind.DOCUMENT,
76
94
  definitions: [
77
95
  {
78
96
  kind: Kind.OPERATION_DEFINITION,
79
97
  operation: OperationTypeNode.QUERY,
98
+ variableDefinitions: usedVariableDefinitions,
80
99
  selectionSet: {
81
100
  kind: Kind.SELECTION_SET,
82
101
  selections: [ introspectionSelection ],
@@ -89,14 +108,21 @@ function makeIntrospectionQueryDocument(introspectionSelection: FieldNode): Docu
89
108
  function executeIntrospection(
90
109
  schema: GraphQLSchema,
91
110
  introspectionSelection: FieldNode,
111
+ variableDefinitions: ReadonlyArray<VariableDefinitionNode> | undefined,
112
+ variableValues: Record<string, any> | undefined,
92
113
  ): any {
93
- const { data } = executeSync({
114
+ const { data, errors } = executeSync({
94
115
  schema,
95
- document: makeIntrospectionQueryDocument(introspectionSelection),
116
+ document: makeIntrospectionQueryDocument(introspectionSelection, variableDefinitions),
96
117
  rootValue: {},
118
+ variableValues,
97
119
  });
120
+ assert(
121
+ !errors || errors.length === 0,
122
+ () => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed but got ${JSON.stringify(errors)}`
123
+ );
98
124
  assert(data, () => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed`);
99
- return data[introspectionSelection.name.value];
125
+ return data[introspectionSelection.alias?.value ?? introspectionSelection.name.value];
100
126
  }
101
127
 
102
128
  export async function executeQueryPlan(
@@ -163,11 +189,17 @@ export async function executeQueryPlan(
163
189
  );
164
190
 
165
191
  let postProcessingErrors: GraphQLError[];
192
+ const variables = requestContext.request.variables;
166
193
  ({ data, errors: postProcessingErrors } = computeResponse({
167
194
  operation,
168
- variables: requestContext.request.variables,
195
+ variables,
169
196
  input: unfilteredData,
170
- introspectionHandling: (f) => executeIntrospection(operationContext.schema, f.expandFragments().toSelectionNode()),
197
+ introspectionHandling: (f) => executeIntrospection(
198
+ operationContext.schema,
199
+ f.expandFragments().toSelectionNode(),
200
+ operationContext.operation.variableDefinitions,
201
+ variables,
202
+ ),
171
203
  }));
172
204
 
173
205
  // If we have errors during the post-processing, we ignore them if any other errors have been thrown during
@@ -376,9 +408,12 @@ async function executeFetch(
376
408
 
377
409
  if (!fetch.requires) {
378
410
  const dataReceivedFromService = await sendOperation(variables);
411
+ if (dataReceivedFromService) {
412
+ applyRewrites(context.supergraphSchema, fetch.outputRewrites, dataReceivedFromService);
413
+ }
379
414
 
380
415
  for (const entity of entities) {
381
- deepMerge(entity, withFetchRewrites(dataReceivedFromService, fetch.outputRewrites));
416
+ deepMerge(entity, dataReceivedFromService);
382
417
  }
383
418
  } else {
384
419
  const requires = fetch.requires;
@@ -394,9 +429,9 @@ async function executeFetch(
394
429
  context.supergraphSchema,
395
430
  entity,
396
431
  requires,
397
- fetch.inputRewrites,
398
432
  );
399
433
  if (representation && representation[TypeNameMetaFieldDef.name]) {
434
+ applyRewrites(context.supergraphSchema, fetch.inputRewrites, representation);
400
435
  representations.push(representation);
401
436
  representationToEntity.push(index);
402
437
  }
@@ -433,8 +468,11 @@ async function executeFetch(
433
468
  );
434
469
  }
435
470
 
471
+
436
472
  for (let i = 0; i < entities.length; i++) {
437
- deepMerge(entities[representationToEntity[i]], withFetchRewrites(receivedEntities[i], filterEntityRewrites(representations[i], fetch.outputRewrites)));
473
+ const receivedEntity = receivedEntities[i];
474
+ applyRewrites(context.supergraphSchema, fetch.outputRewrites, receivedEntity);
475
+ deepMerge(entities[representationToEntity[i]], receivedEntity);
438
476
  }
439
477
  }
440
478
  }
@@ -667,84 +705,6 @@ export function generateHydratedPaths(
667
705
  }
668
706
  }
669
707
 
670
- function applyOrMapRecursive(value: any | any[], fct: (v: any) => any | undefined): any | any[] | undefined {
671
- if (Array.isArray(value)) {
672
- const res = value.map((elt) => applyOrMapRecursive(elt, fct)).filter(isDefined);
673
- return res.length === 0 ? undefined : res;
674
- }
675
- return fct(value);
676
- }
677
-
678
- function withFetchRewrites(fetchResult: ResultMap | null | void, rewrites: FetchDataOutputRewrite[] | undefined): ResultMap | null | void {
679
- if (!rewrites || !fetchResult) {
680
- return fetchResult;
681
- }
682
-
683
- for (const rewrite of rewrites) {
684
- let obj: any = fetchResult;
685
- let i = 0;
686
- while (obj && i < rewrite.path.length - 1) {
687
- const p = rewrite.path[i++];
688
- if (p.startsWith('... on ')) {
689
- const typename = p.slice('... on '.length);
690
- // Filter only objects that match the condition.
691
- obj = applyOrMapRecursive(obj, (elt) => elt[TypeNameMetaFieldDef.name] === typename ? elt : undefined);
692
- } else {
693
- obj = applyOrMapRecursive(obj, (elt) => elt[p]);
694
- }
695
- }
696
- if (obj) {
697
- applyOrMapRecursive(obj, (elt) => {
698
- if (typeof elt === 'object') {
699
- // We need to move the value at path[i] to `renameKeyTo`.
700
- const removedKey = rewrite.path[i];
701
- elt[rewrite.renameKeyTo] = elt[removedKey];
702
- elt[removedKey] = undefined;
703
- }
704
- });
705
- }
706
- }
707
- return fetchResult;
708
- }
709
-
710
- function filterEntityRewrites(entity: Record<string, any>, rewrites: FetchDataOutputRewrite[] | undefined): FetchDataOutputRewrite[] | undefined {
711
- if (!rewrites) {
712
- return undefined;
713
- }
714
-
715
- const typename = entity[TypeNameMetaFieldDef.name] as string;
716
- const typenameAsFragment = `... on ${typename}`;
717
- return rewrites.map((r) => r.path[0] === typenameAsFragment ? { ...r, path: r.path.slice(1) } : undefined).filter(isDefined)
718
- }
719
-
720
- function updateRewrites(rewrites: FetchDataInputRewrite[] | undefined, pathElement: string): {
721
- updated: FetchDataInputRewrite[],
722
- completeRewrite?: any,
723
- } | undefined {
724
- if (!rewrites) {
725
- return undefined;
726
- }
727
-
728
- let completeRewrite: any = undefined;
729
- const updated = rewrites
730
- .map((r) => {
731
- let u: FetchDataInputRewrite | undefined = undefined;
732
- if (r.path[0] === pathElement) {
733
- const updatedPath = r.path.slice(1);
734
- if (updatedPath.length === 0) {
735
- completeRewrite = r.setValueTo;
736
- } else {
737
- u = { ...r, path: updatedPath };
738
- }
739
- }
740
- return u;
741
- })
742
- .filter(isDefined);
743
- return updated.length === 0 && completeRewrite === undefined
744
- ? undefined
745
- : { updated, completeRewrite };
746
- }
747
-
748
708
  /**
749
709
  *
750
710
  * @param source Result of GraphQL execution.
@@ -754,7 +714,6 @@ function executeSelectionSet(
754
714
  schema: GraphQLSchema,
755
715
  source: Record<string, any> | null,
756
716
  selections: QueryPlanSelectionNode[],
757
- activeRewrites?: FetchDataInputRewrite[],
758
717
  ): Record<string, any> | null {
759
718
 
760
719
  // If the underlying service has returned null for the parent (source)
@@ -785,16 +744,10 @@ function executeSelectionSet(
785
744
  return null;
786
745
  }
787
746
 
788
- const updatedRewrites = updateRewrites(activeRewrites, responseName);
789
- if (updatedRewrites?.completeRewrite !== undefined) {
790
- result[responseName] = updatedRewrites.completeRewrite;
791
- continue;
792
- }
793
-
794
747
  if (Array.isArray(source[responseName])) {
795
748
  result[responseName] = source[responseName].map((value: any) =>
796
749
  selections
797
- ? executeSelectionSet(schema, value, selections, updatedRewrites?.updated)
750
+ ? executeSelectionSet(schema, value, selections)
798
751
  : value,
799
752
  );
800
753
  } else if (selections) {
@@ -802,23 +755,18 @@ function executeSelectionSet(
802
755
  schema,
803
756
  source[responseName],
804
757
  selections,
805
- updatedRewrites?.updated,
806
758
  );
807
759
  } else {
808
760
  result[responseName] = source[responseName];
809
761
  }
810
762
  break;
811
763
  case Kind.INLINE_FRAGMENT:
812
- if (!selection.typeCondition) continue;
764
+ if (!selection.typeCondition || !source) continue;
813
765
 
814
- const typename = source && source['__typename'];
815
- if (!typename) continue;
816
-
817
- if (doesTypeConditionMatch(schema, selection.typeCondition, typename)) {
818
- const updatedRewrites = activeRewrites ? updateRewrites(activeRewrites, `... on ${selection.typeCondition}`) : undefined;
766
+ if (isObjectOfType(schema, source, selection.typeCondition)) {
819
767
  deepMerge(
820
768
  result,
821
- executeSelectionSet(schema, source, selection.selections, updatedRewrites?.updated),
769
+ executeSelectionSet(schema, source, selection.selections),
822
770
  );
823
771
  }
824
772
  break;
@@ -828,32 +776,6 @@ function executeSelectionSet(
828
776
  return result;
829
777
  }
830
778
 
831
- function doesTypeConditionMatch(
832
- schema: GraphQLSchema,
833
- typeCondition: string,
834
- typename: string,
835
- ): boolean {
836
- if (typeCondition === typename) {
837
- return true;
838
- }
839
-
840
- const type = schema.getType(typename);
841
- if (!type) {
842
- return false;
843
- }
844
-
845
- const conditionalType = schema.getType(typeCondition);
846
- if (!conditionalType) {
847
- return false;
848
- }
849
-
850
- if (isAbstractType(conditionalType)) {
851
- return (isObjectType(type) || isInterfaceType(type)) && schema.isSubType(conditionalType, type);
852
- }
853
-
854
- return false;
855
- }
856
-
857
779
  function moveIntoCursor(cursor: ResultCursor, pathInCursor: ResponsePath): ResultCursor | undefined {
858
780
  const data = flattenResultsAtPath(cursor.data, pathInCursor);
859
781
  return data ? {