@apollo/gateway 2.3.0-beta.2 → 2.3.0-beta.3
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 +6 -2
- package/dist/executeQueryPlan.d.ts.map +1 -1
- package/dist/executeQueryPlan.js +141 -37
- package/dist/executeQueryPlan.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -17
- package/dist/index.js.map +1 -1
- package/dist/resultShaping.d.ts +12 -0
- package/dist/resultShaping.d.ts.map +1 -0
- package/dist/resultShaping.js +227 -0
- package/dist/resultShaping.js.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/executeQueryPlan.test.ts +358 -64
- package/src/__tests__/execution-utils.ts +1 -0
- package/src/__tests__/resultShaping.test.ts +467 -0
- package/src/executeQueryPlan.ts +251 -61
- package/src/index.ts +4 -28
- package/src/resultShaping.ts +439 -0
- package/src/utilities/__tests__/cleanErrorOfInaccessibleElements.test.ts +0 -107
- package/src/utilities/cleanErrorOfInaccessibleNames.ts +0 -29
package/src/executeQueryPlan.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Headers } from 'node-fetch';
|
|
2
2
|
import {
|
|
3
|
-
execute,
|
|
4
3
|
GraphQLError,
|
|
5
4
|
Kind,
|
|
6
5
|
TypeNameMetaFieldDef,
|
|
@@ -12,6 +11,9 @@ import {
|
|
|
12
11
|
isInterfaceType,
|
|
13
12
|
GraphQLErrorOptions,
|
|
14
13
|
DocumentNode,
|
|
14
|
+
executeSync,
|
|
15
|
+
OperationTypeNode,
|
|
16
|
+
FieldNode,
|
|
15
17
|
} from 'graphql';
|
|
16
18
|
import { Trace, google } from '@apollo/usage-reporting-protobuf';
|
|
17
19
|
import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types';
|
|
@@ -31,8 +33,9 @@ import { deepMerge } from './utilities/deepMerge';
|
|
|
31
33
|
import { isNotNullOrUndefined } from './utilities/array';
|
|
32
34
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
33
35
|
import { OpenTelemetrySpanNames, tracer } from "./utilities/opentelemetry";
|
|
34
|
-
import { assert, defaultRootName, errorCodeDef, ERRORS, isDefined } from '@apollo/federation-internals';
|
|
36
|
+
import { assert, defaultRootName, errorCodeDef, ERRORS, isDefined, operationFromDocument, Schema } from '@apollo/federation-internals';
|
|
35
37
|
import { GatewayGraphQLRequestContext, GatewayExecutionResult } from '@apollo/server-gateway-interface';
|
|
38
|
+
import { computeResponse } from './resultShaping';
|
|
36
39
|
|
|
37
40
|
export type ServiceMap = {
|
|
38
41
|
[serviceName: string]: GraphQLDataSource;
|
|
@@ -40,6 +43,24 @@ export type ServiceMap = {
|
|
|
40
43
|
|
|
41
44
|
type ResultMap = Record<string, any>;
|
|
42
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Represents some "cursor" within the full result, or put another way, a path into the full result and where it points to.
|
|
48
|
+
*
|
|
49
|
+
* Note that results can include lists and the the `path` considered can traverse those lists (the path will have a '@' character) so
|
|
50
|
+
* the data pointed by a cursor is not necessarily a single "branch" of the full results, but is in general a flattened list of all
|
|
51
|
+
* the sub-branches pointed by the path.
|
|
52
|
+
*/
|
|
53
|
+
type ResultCursor = {
|
|
54
|
+
// Path into `fullResult` this cursor is pointing at.
|
|
55
|
+
path: ResponsePath,
|
|
56
|
+
|
|
57
|
+
// The data pointed by this cursor.
|
|
58
|
+
data: ResultMap | ResultMap[],
|
|
59
|
+
|
|
60
|
+
// The full result .
|
|
61
|
+
fullResult: ResultMap,
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
interface ExecutionContext {
|
|
44
65
|
queryPlan: QueryPlan;
|
|
45
66
|
operationContext: OperationContext;
|
|
@@ -49,12 +70,42 @@ interface ExecutionContext {
|
|
|
49
70
|
errors: GraphQLError[];
|
|
50
71
|
}
|
|
51
72
|
|
|
73
|
+
function makeIntrospectionQueryDocument(introspectionSelection: FieldNode): DocumentNode {
|
|
74
|
+
return {
|
|
75
|
+
kind: Kind.DOCUMENT,
|
|
76
|
+
definitions: [
|
|
77
|
+
{
|
|
78
|
+
kind: Kind.OPERATION_DEFINITION,
|
|
79
|
+
operation: OperationTypeNode.QUERY,
|
|
80
|
+
selectionSet: {
|
|
81
|
+
kind: Kind.SELECTION_SET,
|
|
82
|
+
selections: [ introspectionSelection ],
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function executeIntrospection(
|
|
90
|
+
schema: GraphQLSchema,
|
|
91
|
+
introspectionSelection: FieldNode,
|
|
92
|
+
): any {
|
|
93
|
+
const { data } = executeSync({
|
|
94
|
+
schema,
|
|
95
|
+
document: makeIntrospectionQueryDocument(introspectionSelection),
|
|
96
|
+
rootValue: {},
|
|
97
|
+
});
|
|
98
|
+
assert(data, () => `Introspection query for ${JSON.stringify(introspectionSelection)} should not have failed`);
|
|
99
|
+
return data[introspectionSelection.name.value];
|
|
100
|
+
}
|
|
101
|
+
|
|
52
102
|
export async function executeQueryPlan(
|
|
53
103
|
queryPlan: QueryPlan,
|
|
54
104
|
serviceMap: ServiceMap,
|
|
55
105
|
requestContext: GatewayGraphQLRequestContext,
|
|
56
106
|
operationContext: OperationContext,
|
|
57
107
|
supergraphSchema: GraphQLSchema,
|
|
108
|
+
apiSchema: Schema,
|
|
58
109
|
): Promise<GatewayExecutionResult> {
|
|
59
110
|
|
|
60
111
|
const logger = requestContext.logger || console;
|
|
@@ -72,7 +123,7 @@ export async function executeQueryPlan(
|
|
|
72
123
|
errors,
|
|
73
124
|
};
|
|
74
125
|
|
|
75
|
-
|
|
126
|
+
const unfilteredData: ResultMap = Object.create(null);
|
|
76
127
|
|
|
77
128
|
const captureTraces = !!(
|
|
78
129
|
requestContext.metrics && requestContext.metrics.captureTraces
|
|
@@ -82,8 +133,11 @@ export async function executeQueryPlan(
|
|
|
82
133
|
const traceNode = await executeNode(
|
|
83
134
|
context,
|
|
84
135
|
queryPlan.node,
|
|
85
|
-
|
|
86
|
-
|
|
136
|
+
{
|
|
137
|
+
path: [],
|
|
138
|
+
data: unfilteredData,
|
|
139
|
+
fullResult: unfilteredData,
|
|
140
|
+
},
|
|
87
141
|
captureTraces,
|
|
88
142
|
);
|
|
89
143
|
if (captureTraces) {
|
|
@@ -92,27 +146,51 @@ export async function executeQueryPlan(
|
|
|
92
146
|
}
|
|
93
147
|
|
|
94
148
|
const result = await tracer.startActiveSpan(OpenTelemetrySpanNames.POST_PROCESSING, async (span) => {
|
|
95
|
-
|
|
96
|
-
// FIXME: Re-executing the query is a pretty heavy handed way of making sure
|
|
97
|
-
// only explicitly requested fields are included and field ordering follows
|
|
98
|
-
// the original query.
|
|
99
|
-
// It is also used to allow execution of introspection queries though.
|
|
149
|
+
let data;
|
|
100
150
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
document: {
|
|
151
|
+
const operation = operationFromDocument(
|
|
152
|
+
apiSchema,
|
|
153
|
+
{
|
|
105
154
|
kind: Kind.DOCUMENT,
|
|
106
155
|
definitions: [
|
|
107
156
|
operationContext.operation,
|
|
108
157
|
...Object.values(operationContext.fragments),
|
|
109
158
|
],
|
|
110
159
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
160
|
+
{
|
|
161
|
+
validate: false,
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
let postProcessingErrors: GraphQLError[];
|
|
166
|
+
({ data, errors: postProcessingErrors } = computeResponse({
|
|
167
|
+
operation,
|
|
168
|
+
variables: requestContext.request.variables,
|
|
169
|
+
input: unfilteredData,
|
|
170
|
+
introspectionHandling: (f) => executeIntrospection(operationContext.schema, f.expandFragments().toSelectionNode()),
|
|
115
171
|
}));
|
|
172
|
+
|
|
173
|
+
// If we have errors during the post-processing, we ignore them if any other errors have been thrown during
|
|
174
|
+
// query plan execution. That is because in many cases, errors during query plan execution will leave the
|
|
175
|
+
// internal data in a state that triggers additional post-processing errors, but that leads to 2 errors recorded
|
|
176
|
+
// for the same problem and that is unexpected by clients. See https://github.com/apollographql/federation/issues/981
|
|
177
|
+
// for additional context.
|
|
178
|
+
// If we had no errors during query plan execution, then we do ship any post-processing ones as there is little
|
|
179
|
+
// reason not to and it might genuinely help debugging (note that if subgraphs return no errors and we assume that
|
|
180
|
+
// subgraph do return graphQL valid responses, then our composition rules should guarantee no post-processing errors,
|
|
181
|
+
// so getting a post-processing error points to either 1) a bug in our code or in composition or 2) a subgraph not
|
|
182
|
+
// returning valid graphQL results, both of which are well worth surfacing (see [this comment for instance](https://github.com/apollographql/federation/pull/159#issuecomment-801132906))).
|
|
183
|
+
//
|
|
184
|
+
// That said, note that this is still not perfect in the sense that if someone does get subgraph errors, then
|
|
185
|
+
// while postProcessingErrors may duplicate those, it may also contain additional unrelated errors (again, something
|
|
186
|
+
// like a subgraph returning non-grapqlQL valid data unknowingly), and we don't surface those. In a perfect worlds
|
|
187
|
+
// we've be able to filter the post-proessing errors that duplicate errors from subgraph and still ship anything that
|
|
188
|
+
// remains, but it's unclear how to do that at all (it migth be that checking the error path helps, but not sure
|
|
189
|
+
// that's fullproof).
|
|
190
|
+
if (errors.length === 0 && postProcessingErrors.length > 0) {
|
|
191
|
+
span.setStatus({ code:SpanStatusCode.ERROR });
|
|
192
|
+
return { errors: postProcessingErrors, data };
|
|
193
|
+
}
|
|
116
194
|
} catch (error) {
|
|
117
195
|
span.setStatus({ code:SpanStatusCode.ERROR });
|
|
118
196
|
if (error instanceof GraphQLError) {
|
|
@@ -172,11 +250,10 @@ export async function executeQueryPlan(
|
|
|
172
250
|
async function executeNode(
|
|
173
251
|
context: ExecutionContext,
|
|
174
252
|
node: PlanNode,
|
|
175
|
-
|
|
176
|
-
path: ResponsePath,
|
|
253
|
+
currentCursor: ResultCursor | undefined,
|
|
177
254
|
captureTraces: boolean,
|
|
178
255
|
): Promise<Trace.QueryPlanNode> {
|
|
179
|
-
if (!
|
|
256
|
+
if (!currentCursor) {
|
|
180
257
|
// XXX I don't understand `results` threading well enough to understand when this happens
|
|
181
258
|
// and if this corresponds to a real query plan node that should be reported or not.
|
|
182
259
|
//
|
|
@@ -193,8 +270,7 @@ async function executeNode(
|
|
|
193
270
|
const childTraceNode = await executeNode(
|
|
194
271
|
context,
|
|
195
272
|
childNode,
|
|
196
|
-
|
|
197
|
-
path,
|
|
273
|
+
currentCursor,
|
|
198
274
|
captureTraces,
|
|
199
275
|
);
|
|
200
276
|
traceNode.nodes.push(childTraceNode!);
|
|
@@ -203,8 +279,13 @@ async function executeNode(
|
|
|
203
279
|
}
|
|
204
280
|
case 'Parallel': {
|
|
205
281
|
const childTraceNodes = await Promise.all(
|
|
206
|
-
node.nodes.map(async childNode =>
|
|
207
|
-
executeNode(
|
|
282
|
+
node.nodes.map(async (childNode) =>
|
|
283
|
+
executeNode(
|
|
284
|
+
context,
|
|
285
|
+
childNode,
|
|
286
|
+
currentCursor,
|
|
287
|
+
captureTraces,
|
|
288
|
+
),
|
|
208
289
|
),
|
|
209
290
|
);
|
|
210
291
|
return new Trace.QueryPlanNode({
|
|
@@ -225,8 +306,7 @@ async function executeNode(
|
|
|
225
306
|
node: await executeNode(
|
|
226
307
|
context,
|
|
227
308
|
node.node,
|
|
228
|
-
|
|
229
|
-
[...path, ...node.path],
|
|
309
|
+
moveIntoCursor(currentCursor, node.path),
|
|
230
310
|
captureTraces,
|
|
231
311
|
),
|
|
232
312
|
}),
|
|
@@ -241,8 +321,7 @@ async function executeNode(
|
|
|
241
321
|
await executeFetch(
|
|
242
322
|
context,
|
|
243
323
|
node,
|
|
244
|
-
|
|
245
|
-
path,
|
|
324
|
+
currentCursor,
|
|
246
325
|
captureTraces ? traceNode : null,
|
|
247
326
|
);
|
|
248
327
|
} catch (error) {
|
|
@@ -262,8 +341,7 @@ async function executeNode(
|
|
|
262
341
|
async function executeFetch(
|
|
263
342
|
context: ExecutionContext,
|
|
264
343
|
fetch: FetchNode,
|
|
265
|
-
|
|
266
|
-
_path: ResponsePath,
|
|
344
|
+
currentCursor: ResultCursor,
|
|
267
345
|
traceNode: Trace.QueryPlanNode.FetchNode | null,
|
|
268
346
|
): Promise<void> {
|
|
269
347
|
|
|
@@ -277,11 +355,11 @@ async function executeFetch(
|
|
|
277
355
|
}
|
|
278
356
|
|
|
279
357
|
let entities: ResultMap[];
|
|
280
|
-
if (Array.isArray(
|
|
358
|
+
if (Array.isArray(currentCursor.data)) {
|
|
281
359
|
// Remove null or undefined entities from the list
|
|
282
|
-
entities =
|
|
360
|
+
entities = currentCursor.data.filter(isNotNullOrUndefined);
|
|
283
361
|
} else {
|
|
284
|
-
entities = [
|
|
362
|
+
entities = [currentCursor.data];
|
|
285
363
|
}
|
|
286
364
|
|
|
287
365
|
if (entities.length < 1) return;
|
|
@@ -300,13 +378,7 @@ async function executeFetch(
|
|
|
300
378
|
}
|
|
301
379
|
|
|
302
380
|
if (!fetch.requires) {
|
|
303
|
-
const dataReceivedFromService = await sendOperation(
|
|
304
|
-
context,
|
|
305
|
-
fetch.operation,
|
|
306
|
-
variables,
|
|
307
|
-
fetch.operationName,
|
|
308
|
-
fetch.operationDocumentNode
|
|
309
|
-
);
|
|
381
|
+
const dataReceivedFromService = await sendOperation(variables);
|
|
310
382
|
|
|
311
383
|
for (const entity of entities) {
|
|
312
384
|
deepMerge(entity, withFetchRewrites(dataReceivedFromService, fetch.outputRewrites));
|
|
@@ -341,13 +413,7 @@ async function executeFetch(
|
|
|
341
413
|
throw new Error(`Variables cannot contain key "representations"`);
|
|
342
414
|
}
|
|
343
415
|
|
|
344
|
-
const dataReceivedFromService = await sendOperation(
|
|
345
|
-
context,
|
|
346
|
-
fetch.operation,
|
|
347
|
-
{...variables, representations},
|
|
348
|
-
fetch.operationName,
|
|
349
|
-
fetch.operationDocumentNode
|
|
350
|
-
);
|
|
416
|
+
const dataReceivedFromService = await sendOperation({...variables, representations});
|
|
351
417
|
|
|
352
418
|
if (!dataReceivedFromService) {
|
|
353
419
|
return;
|
|
@@ -384,13 +450,11 @@ async function executeFetch(
|
|
|
384
450
|
span.end();
|
|
385
451
|
}
|
|
386
452
|
});
|
|
453
|
+
|
|
387
454
|
async function sendOperation(
|
|
388
|
-
context: ExecutionContext,
|
|
389
|
-
source: string,
|
|
390
455
|
variables: Record<string, any>,
|
|
391
|
-
operationName: string | undefined,
|
|
392
|
-
operationDocumentNode?: DocumentNode
|
|
393
456
|
): Promise<ResultMap | void | null> {
|
|
457
|
+
|
|
394
458
|
// We declare this as 'any' because it is missing url and method, which
|
|
395
459
|
// GraphQLRequest.http is supposed to have if it exists.
|
|
396
460
|
// (This is admittedly kinda weird, since we currently do pass url and
|
|
@@ -420,19 +484,21 @@ async function executeFetch(
|
|
|
420
484
|
const response = await service.process({
|
|
421
485
|
kind: GraphQLDataSourceRequestKind.INCOMING_OPERATION,
|
|
422
486
|
request: {
|
|
423
|
-
query:
|
|
487
|
+
query: fetch.operation,
|
|
424
488
|
variables,
|
|
425
|
-
operationName,
|
|
489
|
+
operationName: fetch.operationName,
|
|
426
490
|
http,
|
|
427
491
|
},
|
|
428
492
|
incomingRequestContext: context.requestContext,
|
|
429
493
|
context: context.requestContext.context,
|
|
430
|
-
document: operationDocumentNode
|
|
494
|
+
document: fetch.operationDocumentNode,
|
|
431
495
|
});
|
|
432
496
|
|
|
433
497
|
if (response.errors) {
|
|
498
|
+
const errorPathHelper = makeLazyErrorPathGenerator(fetch, currentCursor);
|
|
499
|
+
|
|
434
500
|
const errors = response.errors.map((error) =>
|
|
435
|
-
downstreamServiceError(error, fetch.serviceName),
|
|
501
|
+
downstreamServiceError(error, fetch.serviceName, errorPathHelper),
|
|
436
502
|
);
|
|
437
503
|
context.errors.push(...errors);
|
|
438
504
|
}
|
|
@@ -474,7 +540,9 @@ async function executeFetch(
|
|
|
474
540
|
// to have the default names (Query, Mutation, Subscription) even
|
|
475
541
|
// if the implementing services choose different names, so we override
|
|
476
542
|
// whatever the implementing service reported here.
|
|
477
|
-
const rootTypeName = defaultRootName(
|
|
543
|
+
const rootTypeName = defaultRootName(
|
|
544
|
+
context.operationContext.operation.operation,
|
|
545
|
+
);
|
|
478
546
|
traceNode.trace.root?.child?.forEach((child) => {
|
|
479
547
|
child.parentType = rootTypeName;
|
|
480
548
|
});
|
|
@@ -487,6 +555,108 @@ async function executeFetch(
|
|
|
487
555
|
}
|
|
488
556
|
}
|
|
489
557
|
|
|
558
|
+
type ErrorPathGenerator = (
|
|
559
|
+
path: GraphQLErrorOptions['path'],
|
|
560
|
+
) => GraphQLErrorOptions['path'];
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Given response data collected so far and a path such as:
|
|
564
|
+
*
|
|
565
|
+
* ["foo", "@", "bar", "@"]
|
|
566
|
+
*
|
|
567
|
+
* the returned function generates a list of "hydrated" paths, replacing the
|
|
568
|
+
* `"@"` with array indices from the actual data. When we encounter an error in
|
|
569
|
+
* a subgraph fetch, we can use the index in the error's path (e.g.
|
|
570
|
+
* `["_entities", 2, "boom"]`) to look up the appropriate "hydrated" path
|
|
571
|
+
* prefix. The result is something like:
|
|
572
|
+
*
|
|
573
|
+
* ["foo", 1, "bar", 2, "boom"]
|
|
574
|
+
*
|
|
575
|
+
* The returned function is lazy — if we don't encounter errors and it's never
|
|
576
|
+
* called, then we never process the response data to hydrate the paths.
|
|
577
|
+
*
|
|
578
|
+
* This approach is inspired by Apollo Router: https://github.com/apollographql/router/blob/0fd59d2e11cc09e82c876a5fee263b5658cb9539/apollo-router/src/query_planner/fetch.rs#L295-L403
|
|
579
|
+
*/
|
|
580
|
+
function makeLazyErrorPathGenerator(
|
|
581
|
+
fetch: FetchNode,
|
|
582
|
+
cursor: ResultCursor,
|
|
583
|
+
): ErrorPathGenerator {
|
|
584
|
+
let hydratedPaths: ResponsePath[] | undefined;
|
|
585
|
+
|
|
586
|
+
return (errorPath: GraphQLErrorOptions['path']) => {
|
|
587
|
+
if (fetch.requires && typeof errorPath?.[1] === 'number') {
|
|
588
|
+
// only generate paths if we need to look them up via entity index
|
|
589
|
+
if (!hydratedPaths) {
|
|
590
|
+
hydratedPaths = [];
|
|
591
|
+
generateHydratedPaths(
|
|
592
|
+
[],
|
|
593
|
+
cursor.path,
|
|
594
|
+
cursor.fullResult,
|
|
595
|
+
hydratedPaths,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const hydratedPath = hydratedPaths[errorPath[1]] ?? [];
|
|
600
|
+
return [...hydratedPath, ...errorPath.slice(2)];
|
|
601
|
+
} else {
|
|
602
|
+
return errorPath ? [...cursor.path, ...errorPath.slice()] : undefined;
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Given a deeply nested object and a path such as `["foo", "@", "bar", "@"]`,
|
|
609
|
+
* walk the path to build up a list of of "hydrated" paths that match the data,
|
|
610
|
+
* such as:
|
|
611
|
+
*
|
|
612
|
+
* [
|
|
613
|
+
* ["foo", 0, "bar", 0, "boom"],
|
|
614
|
+
* ["foo", 0, "bar", 1, "boom"]
|
|
615
|
+
* ["foo", 1, "bar", 0, "boom"],
|
|
616
|
+
* ["foo", 1, "bar", 1, "boom"]
|
|
617
|
+
* ]
|
|
618
|
+
*/
|
|
619
|
+
export function generateHydratedPaths(
|
|
620
|
+
parent: ResponsePath,
|
|
621
|
+
path: ResponsePath,
|
|
622
|
+
data: ResultMap | null,
|
|
623
|
+
result: ResponsePath[],
|
|
624
|
+
) {
|
|
625
|
+
const head = path[0];
|
|
626
|
+
|
|
627
|
+
if (data == null) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (head == null) { // terminate recursion
|
|
632
|
+
result.push(parent.slice());
|
|
633
|
+
} else if (head === '@') {
|
|
634
|
+
assert(Array.isArray(data), 'expected array when encountering `@`');
|
|
635
|
+
for (const [i, value] of data.entries()) {
|
|
636
|
+
parent.push(i);
|
|
637
|
+
generateHydratedPaths(parent, path.slice(1), value, result);
|
|
638
|
+
parent.pop();
|
|
639
|
+
}
|
|
640
|
+
} else if (typeof head === 'string') {
|
|
641
|
+
if (Array.isArray(data)) {
|
|
642
|
+
for (const [i, value] of data.entries()) {
|
|
643
|
+
parent.push(i);
|
|
644
|
+
generateHydratedPaths(parent, path, value, result);
|
|
645
|
+
parent.pop();
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
if (head in data) {
|
|
649
|
+
const value = data[head];
|
|
650
|
+
parent.push(head);
|
|
651
|
+
generateHydratedPaths(parent, path.slice(1), value, result);
|
|
652
|
+
parent.pop();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
assert(false, `unknown path part "${head}"`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
490
660
|
function applyOrMapRecursive(value: any | any[], fct: (v: any) => any | undefined): any | any[] | undefined {
|
|
491
661
|
if (Array.isArray(value)) {
|
|
492
662
|
const res = value.map((elt) => applyOrMapRecursive(elt, fct)).filter(isDefined);
|
|
@@ -674,7 +844,16 @@ function doesTypeConditionMatch(
|
|
|
674
844
|
return false;
|
|
675
845
|
}
|
|
676
846
|
|
|
677
|
-
function
|
|
847
|
+
function moveIntoCursor(cursor: ResultCursor, pathInCursor: ResponsePath): ResultCursor | undefined {
|
|
848
|
+
const data = flattenResultsAtPath(cursor.data, pathInCursor);
|
|
849
|
+
return data ? {
|
|
850
|
+
path: cursor.path.concat(pathInCursor),
|
|
851
|
+
data,
|
|
852
|
+
fullResult: cursor.fullResult,
|
|
853
|
+
} : undefined;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function flattenResultsAtPath(value: ResultCursor['data'] | undefined | null, path: ResponsePath): ResultCursor['data'] | undefined | null {
|
|
678
857
|
if (path.length === 0) return value;
|
|
679
858
|
if (value === undefined || value === null) return value;
|
|
680
859
|
|
|
@@ -682,6 +861,11 @@ function flattenResultsAtPath(value: any, path: ResponsePath): any {
|
|
|
682
861
|
if (current === '@') {
|
|
683
862
|
return value.flatMap((element: any) => flattenResultsAtPath(element, rest));
|
|
684
863
|
} else {
|
|
864
|
+
assert(typeof current === 'string', () => `Unexpected ${typeof current} found in path`);
|
|
865
|
+
assert(!Array.isArray(value), () => `Unexpected array in result for path element ${current}`);
|
|
866
|
+
// Note that this typecheck because `value[current]` is of type `any` and so the typechecker "trusts us", but in
|
|
867
|
+
// practice this only work because we use this on path that do not point to leaf types, and the `value[current]`
|
|
868
|
+
// is never a base type (non-object nor null/undefined).
|
|
685
869
|
return flattenResultsAtPath(value[current], rest);
|
|
686
870
|
}
|
|
687
871
|
}
|
|
@@ -689,8 +873,10 @@ function flattenResultsAtPath(value: any, path: ResponsePath): any {
|
|
|
689
873
|
function downstreamServiceError(
|
|
690
874
|
originalError: GraphQLFormattedError,
|
|
691
875
|
serviceName: string,
|
|
876
|
+
generateErrorPath: ErrorPathGenerator,
|
|
692
877
|
) {
|
|
693
|
-
let { message
|
|
878
|
+
let { message } = originalError;
|
|
879
|
+
const { extensions } = originalError;
|
|
694
880
|
|
|
695
881
|
if (!message) {
|
|
696
882
|
message = `Error while fetching subquery from service "${serviceName}"`;
|
|
@@ -698,12 +884,13 @@ function downstreamServiceError(
|
|
|
698
884
|
|
|
699
885
|
const errorOptions: GraphQLErrorOptions = {
|
|
700
886
|
originalError: originalError as Error,
|
|
887
|
+
path: generateErrorPath(originalError.path),
|
|
701
888
|
extensions: {
|
|
702
889
|
...extensions,
|
|
703
890
|
// XXX The presence of a serviceName in extensions is used to
|
|
704
891
|
// determine if this error should be captured for metrics reporting.
|
|
705
892
|
serviceName,
|
|
706
|
-
}
|
|
893
|
+
},
|
|
707
894
|
};
|
|
708
895
|
|
|
709
896
|
const codeDef = errorCodeDef(originalError);
|
|
@@ -713,7 +900,10 @@ function downstreamServiceError(
|
|
|
713
900
|
return new GraphQLError(message, errorOptions);
|
|
714
901
|
}
|
|
715
902
|
// Otherwise, we either use the code we found and know, or default to a general downstream error code.
|
|
716
|
-
return (codeDef ?? ERRORS.DOWNSTREAM_SERVICE_ERROR).err(
|
|
903
|
+
return (codeDef ?? ERRORS.DOWNSTREAM_SERVICE_ERROR).err(
|
|
904
|
+
message,
|
|
905
|
+
errorOptions,
|
|
906
|
+
);
|
|
717
907
|
}
|
|
718
908
|
|
|
719
909
|
export const defaultFieldResolverWithAliasSupport: GraphQLFieldResolver<
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,6 @@ import { createHash } from '@apollo/utils.createhash';
|
|
|
3
3
|
import type { Logger } from '@apollo/utils.logger';
|
|
4
4
|
import LRUCache from 'lru-cache';
|
|
5
5
|
import {
|
|
6
|
-
isObjectType,
|
|
7
|
-
isIntrospectionType,
|
|
8
6
|
GraphQLSchema,
|
|
9
7
|
VariableDefinitionNode,
|
|
10
8
|
} from 'graphql';
|
|
@@ -12,7 +10,6 @@ import { buildOperationContext, OperationContext } from './operationContext';
|
|
|
12
10
|
import {
|
|
13
11
|
executeQueryPlan,
|
|
14
12
|
ServiceMap,
|
|
15
|
-
defaultFieldResolverWithAliasSupport,
|
|
16
13
|
} from './executeQueryPlan';
|
|
17
14
|
import {
|
|
18
15
|
GraphQLDataSource,
|
|
@@ -118,8 +115,8 @@ export class ApolloGateway implements GatewayInterface {
|
|
|
118
115
|
public schema?: GraphQLSchema;
|
|
119
116
|
// Same as a `schema` but as a `Schema` to avoid reconverting when we need it.
|
|
120
117
|
// TODO(sylvain): if we add caching in `Schema.toGraphQLJSSchema`, we could maybe only keep `apiSchema`
|
|
121
|
-
// and make `schema` a getter (though `schema` does
|
|
122
|
-
// be accounted for
|
|
118
|
+
// and make `schema` a getter (though `schema` does add some extension and this should
|
|
119
|
+
// be accounted for). Unsure if moving from a member to a getter could break anyone externally however
|
|
123
120
|
// (also unclear why we expose a mutable member public in the first place; don't everything break if the
|
|
124
121
|
// use manually assigns `schema`?).
|
|
125
122
|
private apiSchema?: Schema;
|
|
@@ -559,9 +556,7 @@ export class ApolloGateway implements GatewayInterface {
|
|
|
559
556
|
): void {
|
|
560
557
|
this.queryPlanStore.clear();
|
|
561
558
|
this.apiSchema = coreSchema.toAPISchema();
|
|
562
|
-
this.schema = addExtensions(
|
|
563
|
-
wrapSchemaWithAliasResolver(this.apiSchema.toGraphQLJSSchema()),
|
|
564
|
-
);
|
|
559
|
+
this.schema = addExtensions(this.apiSchema.toGraphQLJSSchema());
|
|
565
560
|
this.queryPlanner = new QueryPlanner(coreSchema, this.config.queryPlannerConfig);
|
|
566
561
|
|
|
567
562
|
// Notify onSchemaChange listeners of the updated schema
|
|
@@ -828,6 +823,7 @@ export class ApolloGateway implements GatewayInterface {
|
|
|
828
823
|
requestContext,
|
|
829
824
|
operationContext,
|
|
830
825
|
this.supergraphSchema!,
|
|
826
|
+
this.apiSchema!,
|
|
831
827
|
);
|
|
832
828
|
|
|
833
829
|
const shouldShowQueryPlan =
|
|
@@ -984,26 +980,6 @@ function approximateObjectSize<T>(obj: T): number {
|
|
|
984
980
|
return Buffer.byteLength(JSON.stringify(obj), 'utf8');
|
|
985
981
|
}
|
|
986
982
|
|
|
987
|
-
// We can't use transformSchema here because the extension data for query
|
|
988
|
-
// planning would be lost. Instead we set a resolver for each field
|
|
989
|
-
// in order to counteract GraphQLExtensions preventing a defaultFieldResolver
|
|
990
|
-
// from doing the same job
|
|
991
|
-
function wrapSchemaWithAliasResolver(schema: GraphQLSchema): GraphQLSchema {
|
|
992
|
-
const typeMap = schema.getTypeMap();
|
|
993
|
-
Object.keys(typeMap).forEach((typeName) => {
|
|
994
|
-
const type = typeMap[typeName];
|
|
995
|
-
|
|
996
|
-
if (isObjectType(type) && !isIntrospectionType(type)) {
|
|
997
|
-
const fields = type.getFields();
|
|
998
|
-
Object.keys(fields).forEach((fieldName) => {
|
|
999
|
-
const field = fields[fieldName];
|
|
1000
|
-
field.resolve = defaultFieldResolverWithAliasSupport;
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
});
|
|
1004
|
-
return schema;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
983
|
// Throw this in places that should be unreachable (because all other cases have
|
|
1008
984
|
// been handled, reducing the type of the argument to `never`). TypeScript will
|
|
1009
985
|
// complain if in fact there is a valid type for the argument.
|