@apollo/gateway 2.4.0 → 2.4.2

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.
@@ -62,7 +62,7 @@ describe('Using supergraphSdl static configuration', () => {
62
62
 
63
63
  nock(accounts.url)
64
64
  .post('/', { query: '{me{username}}', variables: {} })
65
- .reply(200, { data: { me: { username: '@jbaxleyiii' } } });
65
+ .reply(200, { data: { me: { username: '@apollo-user' } } });
66
66
 
67
67
 
68
68
  const result = await server.executeOperation({
@@ -72,7 +72,7 @@ describe('Using supergraphSdl static configuration', () => {
72
72
  expect(unwrapSingleResultKind(result).data).toMatchInlineSnapshot(`
73
73
  Object {
74
74
  "me": Object {
75
- "username": "@jbaxleyiii",
75
+ "username": "@apollo-user",
76
76
  },
77
77
  }
78
78
  `);
@@ -6,8 +6,8 @@ expect.addSnapshotSerializer(astSerializer);
6
6
  expect.addSnapshotSerializer(queryPlanSerializer);
7
7
 
8
8
  const users = [
9
- { id: '1', name: 'Trevor Scheer', organizationId: '1', __typename: 'User' },
10
- { id: '1', name: 'Trevor Scheer', organizationId: '2', __typename: 'User' },
9
+ { id: '1', name: 'Trevor', organizationId: '1', __typename: 'User' },
10
+ { id: '1', name: 'Trevor', organizationId: '2', __typename: 'User' },
11
11
  { id: '2', name: 'James Baxley', organizationId: '1', __typename: 'User' },
12
12
  { id: '2', name: 'James Baxley', organizationId: '3', __typename: 'User' },
13
13
  ];
@@ -162,7 +162,7 @@ it('works fetches data correctly with complex / nested @key fields', async () =>
162
162
  reviews: [
163
163
  {
164
164
  author: {
165
- name: 'Trevor Scheer',
165
+ name: 'Trevor',
166
166
  organization: {
167
167
  name: 'Apollo',
168
168
  },
@@ -170,7 +170,7 @@ it('works fetches data correctly with complex / nested @key fields', async () =>
170
170
  },
171
171
  {
172
172
  author: {
173
- name: 'Trevor Scheer',
173
+ name: 'Trevor',
174
174
  organization: {
175
175
  name: 'Wayfair',
176
176
  },
@@ -6,7 +6,7 @@ expect.addSnapshotSerializer(astSerializer);
6
6
  expect.addSnapshotSerializer(queryPlanSerializer);
7
7
 
8
8
  const users = [
9
- { id: ['1', '1'], name: 'Trevor Scheer', __typename: 'User' },
9
+ { id: ['1', '1'], name: 'Trevor', __typename: 'User' },
10
10
  { id: ['2', '2'], name: 'James Baxley', __typename: 'User' },
11
11
  ];
12
12
 
@@ -89,7 +89,7 @@ it('fetches data correctly list type @key fields', async () => {
89
89
 
90
90
  expect(data).toEqual({
91
91
  reviews: [
92
- { body: 'Good', author: { name: 'Trevor Scheer' } },
92
+ { body: 'Good', author: { name: 'Trevor' } },
93
93
  { body: 'Bad', author: { name: 'James Baxley' } },
94
94
  ],
95
95
  });
@@ -7,7 +7,7 @@ expect.addSnapshotSerializer(queryPlanSerializer);
7
7
 
8
8
  const users = [
9
9
  { ssn: '111-11-1111', name: 'Trevor', id: '10', __typename: 'User' },
10
- { ssn: '222-22-2222', name: 'Scheer', id: '20', __typename: 'User' },
10
+ { ssn: '222-22-2222', name: 'Joel', id: '20', __typename: 'User' },
11
11
  { ssn: '333-33-3333', name: 'James', id: '30', __typename: 'User' },
12
12
  { ssn: '444-44-4444', name: 'Baxley', id: '40', __typename: 'User' },
13
13
  ];
@@ -148,7 +148,7 @@ it('fetches data correctly with multiple @key fields', async () => {
148
148
  body: 'B',
149
149
  author: {
150
150
  risk: 0.2,
151
- name: 'Scheer',
151
+ name: 'Joel',
152
152
  },
153
153
  },
154
154
  {
@@ -218,13 +218,13 @@ it('collapses nested requires with user-defined fragments', async () => {
218
218
  {
219
219
  user {
220
220
  __typename
221
- id
222
221
  preferences {
223
222
  favorites {
224
- animal
225
223
  color
224
+ animal
226
225
  }
227
226
  }
227
+ id
228
228
  }
229
229
  }
230
230
  },
@@ -18,7 +18,7 @@ const accounts = {
18
18
  `,
19
19
  resolvers: {
20
20
  Query: {
21
- me: () => ({ id: 1, name: 'Martijn' }),
21
+ me: () => ({ id: 1, name: 'Me' }),
22
22
  },
23
23
  },
24
24
  };
@@ -98,14 +98,7 @@ describe('value types', () => {
98
98
  reviews {
99
99
  metadata {
100
100
  __typename
101
- ... on KeyValue {
102
- key
103
- value
104
- }
105
- ... on Error {
106
- code
107
- message
108
- }
101
+ ...Metadata
109
102
  }
110
103
  }
111
104
  }
@@ -113,18 +106,22 @@ describe('value types', () => {
113
106
  reviews {
114
107
  metadata {
115
108
  __typename
116
- ... on KeyValue {
117
- key
118
- value
119
- }
120
- ... on Error {
121
- code
122
- message
123
- }
109
+ ...Metadata
124
110
  }
125
111
  }
126
112
  }
127
113
  }
114
+
115
+ fragment Metadata on MetadataOrError {
116
+ ... on KeyValue {
117
+ key
118
+ value
119
+ }
120
+ ... on Error {
121
+ code
122
+ message
123
+ }
124
+ }
128
125
  },
129
126
  },
130
127
  Flatten(path: "topProducts.@") {
@@ -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.expandAllFragments().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 ? {