@apollo/gateway 2.4.0-alpha.1 → 2.4.1

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 (30) hide show
  1. package/dist/__generated__/graphqlTypes.d.ts +1 -0
  2. package/dist/__generated__/graphqlTypes.d.ts.map +1 -1
  3. package/dist/__generated__/graphqlTypes.js.map +1 -1
  4. package/dist/dataRewrites.d.ts +5 -0
  5. package/dist/dataRewrites.d.ts.map +1 -0
  6. package/dist/dataRewrites.js +103 -0
  7. package/dist/dataRewrites.js.map +1 -0
  8. package/dist/executeQueryPlan.d.ts.map +1 -1
  9. package/dist/executeQueryPlan.js +41 -110
  10. package/dist/executeQueryPlan.js.map +1 -1
  11. package/dist/resultShaping.js +3 -3
  12. package/dist/resultShaping.js.map +1 -1
  13. package/dist/supergraphManagers/UplinkSupergraphManager/index.js +1 -1
  14. package/dist/supergraphManagers/UplinkSupergraphManager/index.js.map +1 -1
  15. package/package.json +4 -4
  16. package/src/__generated__/graphqlTypes.ts +4 -3
  17. package/src/__tests__/buildQueryPlan.test.ts +0 -1
  18. package/src/__tests__/executeQueryPlan.introspection.test.ts +140 -0
  19. package/src/__tests__/executeQueryPlan.test.ts +269 -5
  20. package/src/__tests__/gateway/buildService.test.ts +2 -2
  21. package/src/__tests__/gateway/supergraphSdl.test.ts +2 -2
  22. package/src/__tests__/integration/complex-key.test.ts +4 -4
  23. package/src/__tests__/integration/list-key.test.ts +2 -2
  24. package/src/__tests__/integration/multiple-key.test.ts +2 -2
  25. package/src/__tests__/integration/requires.test.ts +2 -2
  26. package/src/__tests__/integration/single-service.test.ts +1 -1
  27. package/src/__tests__/integration/value-types.test.ts +13 -16
  28. package/src/dataRewrites.ts +130 -0
  29. package/src/executeQueryPlan.ts +57 -135
  30. package/src/resultShaping.ts +3 -3
@@ -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,17 +26,16 @@ import {
26
26
  QueryPlanSelectionNode,
27
27
  QueryPlanFieldNode,
28
28
  getResponseName,
29
- FetchDataInputRewrite,
30
- FetchDataOutputRewrite,
31
29
  evaluateCondition,
32
30
  } from '@apollo/query-planner';
33
31
  import { deepMerge } from './utilities/deepMerge';
34
32
  import { isNotNullOrUndefined } from './utilities/array';
35
33
  import { SpanStatusCode } from "@opentelemetry/api";
36
34
  import { OpenTelemetrySpanNames, tracer } from "./utilities/opentelemetry";
37
- import { assert, defaultRootName, errorCodeDef, ERRORS, isDefined, Operation, operationFromDocument, Schema } from '@apollo/federation-internals';
35
+ import { assert, defaultRootName, errorCodeDef, ERRORS, Operation, operationFromDocument, Schema } from '@apollo/federation-internals';
38
36
  import { GatewayGraphQLRequestContext, GatewayExecutionResult } from '@apollo/server-gateway-interface';
39
37
  import { computeResponse } from './resultShaping';
38
+ import { applyRewrites, isObjectOfType } from './dataRewrites';
40
39
 
41
40
  export type ServiceMap = {
42
41
  [serviceName: string]: GraphQLDataSource;
@@ -72,13 +71,33 @@ interface ExecutionContext {
72
71
  errors: GraphQLError[];
73
72
  }
74
73
 
75
- function makeIntrospectionQueryDocument(introspectionSelection: FieldNode): DocumentNode {
74
+ function collectUsedVariables(node: ASTNode): Set<string> {
75
+ const usedVariables = new Set<string>();
76
+ visit(node, {
77
+ Variable: ({ name }) => {
78
+ usedVariables.add(name.value);
79
+ }
80
+ });
81
+ return usedVariables;
82
+ }
83
+
84
+ function makeIntrospectionQueryDocument(
85
+ introspectionSelection: FieldNode,
86
+ variableDefinitions?: readonly VariableDefinitionNode[],
87
+ ): DocumentNode {
88
+ const usedVariables = collectUsedVariables(introspectionSelection);
89
+ const usedVariableDefinitions = variableDefinitions?.filter((def) => usedVariables.has(def.variable.name.value));
90
+ assert(
91
+ usedVariables.size === (usedVariableDefinitions?.length ?? 0),
92
+ () => `Should have found all used variables ${[...usedVariables]} in definitions ${JSON.stringify(variableDefinitions)}`,
93
+ );
76
94
  return {
77
95
  kind: Kind.DOCUMENT,
78
96
  definitions: [
79
97
  {
80
98
  kind: Kind.OPERATION_DEFINITION,
81
99
  operation: OperationTypeNode.QUERY,
100
+ variableDefinitions: usedVariableDefinitions,
82
101
  selectionSet: {
83
102
  kind: Kind.SELECTION_SET,
84
103
  selections: [ introspectionSelection ],
@@ -91,14 +110,21 @@ function makeIntrospectionQueryDocument(introspectionSelection: FieldNode): Docu
91
110
  function executeIntrospection(
92
111
  schema: GraphQLSchema,
93
112
  introspectionSelection: FieldNode,
113
+ variableDefinitions: ReadonlyArray<VariableDefinitionNode> | undefined,
114
+ variableValues: Record<string, any> | undefined,
94
115
  ): any {
95
- const { data } = executeSync({
116
+ const { data, errors } = executeSync({
96
117
  schema,
97
- document: makeIntrospectionQueryDocument(introspectionSelection),
118
+ document: makeIntrospectionQueryDocument(introspectionSelection, variableDefinitions),
98
119
  rootValue: {},
120
+ variableValues,
99
121
  });
122
+ assert(
123
+ !errors || errors.length === 0,
124
+ () => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed but got ${JSON.stringify(errors)}`
125
+ );
100
126
  assert(data, () => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed`);
101
- return data[introspectionSelection.name.value];
127
+ return data[introspectionSelection.alias?.value ?? introspectionSelection.name.value];
102
128
  }
103
129
 
104
130
  export async function executeQueryPlan(
@@ -185,11 +211,17 @@ export async function executeQueryPlan(
185
211
  let data;
186
212
  try {
187
213
  let postProcessingErrors: GraphQLError[];
214
+ const variables = requestContext.request.variables;
188
215
  ({ data, errors: postProcessingErrors } = computeResponse({
189
216
  operation,
190
- variables: requestContext.request.variables,
217
+ variables,
191
218
  input: unfilteredData,
192
- introspectionHandling: (f) => executeIntrospection(operationContext.schema, f.expandFragments().toSelectionNode()),
219
+ introspectionHandling: (f) => executeIntrospection(
220
+ operationContext.schema,
221
+ f.expandAllFragments().toSelectionNode(),
222
+ operationContext.operation.variableDefinitions,
223
+ variables,
224
+ ),
193
225
  }));
194
226
 
195
227
  // If we have errors during the post-processing, we ignore them if any other errors have been thrown during
@@ -416,9 +448,12 @@ async function executeFetch(
416
448
 
417
449
  if (!fetch.requires) {
418
450
  const dataReceivedFromService = await sendOperation(variables);
451
+ if (dataReceivedFromService) {
452
+ applyRewrites(context.supergraphSchema, fetch.outputRewrites, dataReceivedFromService);
453
+ }
419
454
 
420
455
  for (const entity of entities) {
421
- deepMerge(entity, withFetchRewrites(dataReceivedFromService, fetch.outputRewrites));
456
+ deepMerge(entity, dataReceivedFromService);
422
457
  }
423
458
  } else {
424
459
  const requires = fetch.requires;
@@ -434,9 +469,9 @@ async function executeFetch(
434
469
  context.supergraphSchema,
435
470
  entity,
436
471
  requires,
437
- fetch.inputRewrites,
438
472
  );
439
473
  if (representation && representation[TypeNameMetaFieldDef.name]) {
474
+ applyRewrites(context.supergraphSchema, fetch.inputRewrites, representation);
440
475
  representations.push(representation);
441
476
  representationToEntity.push(index);
442
477
  }
@@ -473,8 +508,11 @@ async function executeFetch(
473
508
  );
474
509
  }
475
510
 
511
+
476
512
  for (let i = 0; i < entities.length; i++) {
477
- deepMerge(entities[representationToEntity[i]], withFetchRewrites(receivedEntities[i], filterEntityRewrites(representations[i], fetch.outputRewrites)));
513
+ const receivedEntity = receivedEntities[i];
514
+ applyRewrites(context.supergraphSchema, fetch.outputRewrites, receivedEntity);
515
+ deepMerge(entities[representationToEntity[i]], receivedEntity);
478
516
  }
479
517
  }
480
518
  }
@@ -707,84 +745,6 @@ export function generateHydratedPaths(
707
745
  }
708
746
  }
709
747
 
710
- function applyOrMapRecursive(value: any | any[], fct: (v: any) => any | undefined): any | any[] | undefined {
711
- if (Array.isArray(value)) {
712
- const res = value.map((elt) => applyOrMapRecursive(elt, fct)).filter(isDefined);
713
- return res.length === 0 ? undefined : res;
714
- }
715
- return fct(value);
716
- }
717
-
718
- function withFetchRewrites(fetchResult: ResultMap | null | void, rewrites: FetchDataOutputRewrite[] | undefined): ResultMap | null | void {
719
- if (!rewrites || !fetchResult) {
720
- return fetchResult;
721
- }
722
-
723
- for (const rewrite of rewrites) {
724
- let obj: any = fetchResult;
725
- let i = 0;
726
- while (obj && i < rewrite.path.length - 1) {
727
- const p = rewrite.path[i++];
728
- if (p.startsWith('... on ')) {
729
- const typename = p.slice('... on '.length);
730
- // Filter only objects that match the condition.
731
- obj = applyOrMapRecursive(obj, (elt) => elt[TypeNameMetaFieldDef.name] === typename ? elt : undefined);
732
- } else {
733
- obj = applyOrMapRecursive(obj, (elt) => elt[p]);
734
- }
735
- }
736
- if (obj) {
737
- applyOrMapRecursive(obj, (elt) => {
738
- if (typeof elt === 'object') {
739
- // We need to move the value at path[i] to `renameKeyTo`.
740
- const removedKey = rewrite.path[i];
741
- elt[rewrite.renameKeyTo] = elt[removedKey];
742
- elt[removedKey] = undefined;
743
- }
744
- });
745
- }
746
- }
747
- return fetchResult;
748
- }
749
-
750
- function filterEntityRewrites(entity: Record<string, any>, rewrites: FetchDataOutputRewrite[] | undefined): FetchDataOutputRewrite[] | undefined {
751
- if (!rewrites) {
752
- return undefined;
753
- }
754
-
755
- const typename = entity[TypeNameMetaFieldDef.name] as string;
756
- const typenameAsFragment = `... on ${typename}`;
757
- return rewrites.map((r) => r.path[0] === typenameAsFragment ? { ...r, path: r.path.slice(1) } : undefined).filter(isDefined)
758
- }
759
-
760
- function updateRewrites(rewrites: FetchDataInputRewrite[] | undefined, pathElement: string): {
761
- updated: FetchDataInputRewrite[],
762
- completeRewrite?: any,
763
- } | undefined {
764
- if (!rewrites) {
765
- return undefined;
766
- }
767
-
768
- let completeRewrite: any = undefined;
769
- const updated = rewrites
770
- .map((r) => {
771
- let u: FetchDataInputRewrite | undefined = undefined;
772
- if (r.path[0] === pathElement) {
773
- const updatedPath = r.path.slice(1);
774
- if (updatedPath.length === 0) {
775
- completeRewrite = r.setValueTo;
776
- } else {
777
- u = { ...r, path: updatedPath };
778
- }
779
- }
780
- return u;
781
- })
782
- .filter(isDefined);
783
- return updated.length === 0 && completeRewrite === undefined
784
- ? undefined
785
- : { updated, completeRewrite };
786
- }
787
-
788
748
  /**
789
749
  *
790
750
  * @param source Result of GraphQL execution.
@@ -794,7 +754,6 @@ function executeSelectionSet(
794
754
  schema: GraphQLSchema,
795
755
  source: Record<string, any> | null,
796
756
  selections: QueryPlanSelectionNode[],
797
- activeRewrites?: FetchDataInputRewrite[],
798
757
  ): Record<string, any> | null {
799
758
 
800
759
  // If the underlying service has returned null for the parent (source)
@@ -825,16 +784,10 @@ function executeSelectionSet(
825
784
  return null;
826
785
  }
827
786
 
828
- const updatedRewrites = updateRewrites(activeRewrites, responseName);
829
- if (updatedRewrites?.completeRewrite !== undefined) {
830
- result[responseName] = updatedRewrites.completeRewrite;
831
- continue;
832
- }
833
-
834
787
  if (Array.isArray(source[responseName])) {
835
788
  result[responseName] = source[responseName].map((value: any) =>
836
789
  selections
837
- ? executeSelectionSet(schema, value, selections, updatedRewrites?.updated)
790
+ ? executeSelectionSet(schema, value, selections)
838
791
  : value,
839
792
  );
840
793
  } else if (selections) {
@@ -842,23 +795,18 @@ function executeSelectionSet(
842
795
  schema,
843
796
  source[responseName],
844
797
  selections,
845
- updatedRewrites?.updated,
846
798
  );
847
799
  } else {
848
800
  result[responseName] = source[responseName];
849
801
  }
850
802
  break;
851
803
  case Kind.INLINE_FRAGMENT:
852
- if (!selection.typeCondition) continue;
804
+ if (!selection.typeCondition || !source) continue;
853
805
 
854
- const typename = source && source['__typename'];
855
- if (!typename) continue;
856
-
857
- if (doesTypeConditionMatch(schema, selection.typeCondition, typename)) {
858
- const updatedRewrites = activeRewrites ? updateRewrites(activeRewrites, `... on ${selection.typeCondition}`) : undefined;
806
+ if (isObjectOfType(schema, source, selection.typeCondition)) {
859
807
  deepMerge(
860
808
  result,
861
- executeSelectionSet(schema, source, selection.selections, updatedRewrites?.updated),
809
+ executeSelectionSet(schema, source, selection.selections),
862
810
  );
863
811
  }
864
812
  break;
@@ -868,32 +816,6 @@ function executeSelectionSet(
868
816
  return result;
869
817
  }
870
818
 
871
- function doesTypeConditionMatch(
872
- schema: GraphQLSchema,
873
- typeCondition: string,
874
- typename: string,
875
- ): boolean {
876
- if (typeCondition === typename) {
877
- return true;
878
- }
879
-
880
- const type = schema.getType(typename);
881
- if (!type) {
882
- return false;
883
- }
884
-
885
- const conditionalType = schema.getType(typeCondition);
886
- if (!conditionalType) {
887
- return false;
888
- }
889
-
890
- if (isAbstractType(conditionalType)) {
891
- return (isObjectType(type) || isInterfaceType(type)) && schema.isSubType(conditionalType, type);
892
- }
893
-
894
- return false;
895
- }
896
-
897
819
  function moveIntoCursor(cursor: ResultCursor, pathInCursor: ResponsePath): ResultCursor | undefined {
898
820
  const data = flattenResultsAtPath(cursor.data, pathInCursor);
899
821
  return data ? {
@@ -161,12 +161,12 @@ function applySelectionSet({
161
161
  parentType: CompositeType,
162
162
  }): ApplyResult {
163
163
  for (const selection of selectionSet.selections()) {
164
- if (shouldSkip(selection.element(), parameters)) {
164
+ if (shouldSkip(selection.element, parameters)) {
165
165
  continue;
166
166
  }
167
167
 
168
168
  if (selection.kind === 'FieldSelection') {
169
- const field = selection.element();
169
+ const field = selection.element;
170
170
  const fieldType = field.definition.type!;
171
171
  const responseName = field.responseName();
172
172
  const outputValue = output[responseName];
@@ -241,7 +241,7 @@ function applySelectionSet({
241
241
  return ApplyResult.NULL_BUBBLE_UP;
242
242
  }
243
243
  } else {
244
- const fragment = selection.element();
244
+ const fragment = selection.element;
245
245
  const typename = input[typenameFieldName];
246
246
  assert(!typename || typeof typename === 'string', () => `Got unexpected value for __typename: ${typename}`);
247
247
  if (typeConditionApplies(parameters.schema, fragment.typeCondition, typename, parentType)) {