Skip to content

Commit efd25b6

Browse files
committed
refactor(errors): proxy all the errors
Previously, errors with invalid or missing paths were lost. See: #1641 https://github.com/apollographql/apollo-server/issues/4226 They are now saved! All correctly pathed errors are now inlined into the returned data by the CheckResultAndHandleErrors transform. The data is now annotated only with the remaining "unpathed" errors. These are then returned when possible if a null is encountered including the missing or potentially invalid path. Changes to error handling obviate some existing functions including getErrorsByPathSegment, getErrors in favor of getUnpathedErrors. Utility functions including slicedError and unreleased functions extendedError and unextendedError are no longer necessary.
1 parent 3fc37b3 commit efd25b6

File tree

12 files changed

+269
-198
lines changed

12 files changed

+269
-198
lines changed

packages/delegate/src/defaultMergedResolver.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getResponseKeyFromInfo } from '@graphql-tools/utils';
44

55
import { resolveExternalValue } from './resolveExternalValue';
66
import { getSubschema } from './Subschema';
7-
import { getErrors, isExternalData } from './externalData';
7+
import { getUnpathedErrors, isExternalData } from './externalData';
88
import { ExternalData } from './types';
99

1010
/**
@@ -32,8 +32,8 @@ export function defaultMergedResolver(
3232
}
3333

3434
const data = parent[responseKey];
35+
const unpathedErrors = getUnpathedErrors(parent);
3536
const subschema = getSubschema(parent, responseKey);
36-
const errors = getErrors(parent, responseKey);
3737

38-
return resolveExternalValue(data, errors, subschema, context, info);
38+
return resolveExternalValue(data, unpathedErrors, subschema, context, info);
3939
}

packages/delegate/src/externalData.ts

+9-55
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
import { GraphQLSchema, GraphQLError, GraphQLObjectType, SelectionSetNode } from 'graphql';
22

3-
import {
4-
slicedError,
5-
extendedError,
6-
mergeDeep,
7-
relocatedError,
8-
GraphQLExecutionContext,
9-
collectFields,
10-
} from '@graphql-tools/utils';
3+
import { mergeDeep, relocatedError, GraphQLExecutionContext, collectFields } from '@graphql-tools/utils';
114

125
import { SubschemaConfig, ExternalData } from './types';
13-
import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, ERROR_SYMBOL } from './symbols';
6+
import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols';
147

158
export function isExternalData(data: any): data is ExternalData {
16-
return data[ERROR_SYMBOL] !== undefined;
9+
return data[UNPATHED_ERRORS_SYMBOL] !== undefined;
1710
}
1811

1912
export function annotateExternalData(
@@ -24,7 +17,7 @@ export function annotateExternalData(
2417
Object.defineProperties(data, {
2518
[OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema },
2619
[FIELD_SUBSCHEMA_MAP_SYMBOL]: { value: Object.create(null) },
27-
[ERROR_SYMBOL]: { value: errors },
20+
[UNPATHED_ERRORS_SYMBOL]: { value: errors },
2821
});
2922
return data;
3023
}
@@ -33,39 +26,8 @@ export function getSubschema(data: ExternalData, responseKey: string): GraphQLSc
3326
return data[FIELD_SUBSCHEMA_MAP_SYMBOL][responseKey] ?? data[OBJECT_SUBSCHEMA_SYMBOL];
3427
}
3528

36-
export function getErrors(data: ExternalData, pathSegment: string): Array<GraphQLError> {
37-
const errors = data == null ? data : data[ERROR_SYMBOL];
38-
39-
if (!Array.isArray(errors)) {
40-
return null;
41-
}
42-
43-
const fieldErrors = [];
44-
45-
for (const error of errors) {
46-
if (!error.path || error.path[0] === pathSegment) {
47-
fieldErrors.push(error);
48-
}
49-
}
50-
51-
return fieldErrors;
52-
}
53-
54-
export function getErrorsByPathSegment(errors: ReadonlyArray<GraphQLError>): Record<string, Array<GraphQLError>> {
55-
const record = Object.create(null);
56-
errors.forEach(error => {
57-
if (!error.path || error.path.length < 2) {
58-
return;
59-
}
60-
61-
const pathSegment = error.path[1];
62-
63-
const current = pathSegment in record ? record[pathSegment] : [];
64-
current.push(slicedError(error));
65-
record[pathSegment] = current;
66-
});
67-
68-
return record;
29+
export function getUnpathedErrors(data: ExternalData): Array<GraphQLError> {
30+
return data[UNPATHED_ERRORS_SYMBOL];
6931
}
7032

7133
export function mergeExternalData(
@@ -95,12 +57,11 @@ export function mergeExternalData(
9557
);
9658
const nullResult = {};
9759
Object.keys(fieldNodes).forEach(responseKey => {
98-
errors.push(relocatedError(source, [responseKey]));
99-
nullResult[responseKey] = null;
60+
nullResult[responseKey] = relocatedError(source, path.concat([responseKey]));
10061
});
10162
results.push(nullResult);
10263
} else {
103-
errors = errors.concat(source[ERROR_SYMBOL]);
64+
errors = errors.concat(source[UNPATHED_ERRORS_SYMBOL]);
10465
results.push(source);
10566
}
10667
});
@@ -118,14 +79,7 @@ export function mergeExternalData(
11879
? Object.assign({}, target[FIELD_SUBSCHEMA_MAP_SYMBOL], fieldSubschemaMap)
11980
: fieldSubschemaMap;
12081

121-
const annotatedErrors = errors.map(error => {
122-
return extendedError(error, {
123-
...error.extensions,
124-
graphQLToolsMergedPath: error.path != null ? [...path, ...error.path] : path,
125-
});
126-
});
127-
128-
result[ERROR_SYMBOL] = target[ERROR_SYMBOL].concat(annotatedErrors);
82+
result[UNPATHED_ERRORS_SYMBOL] = target[UNPATHED_ERRORS_SYMBOL].concat(errors);
12983

13084
return result;
13185
}

packages/delegate/src/resolveExternalValue/handleList.ts

+11-10
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,24 @@ import {
1111
} from 'graphql';
1212

1313
import { SubschemaConfig } from '../types';
14-
import { getErrorsByPathSegment } from '../externalData';
1514

1615
import { handleNull } from './handleNull';
1716
import { handleObject } from './handleObject';
1817

1918
export function handleList(
2019
type: GraphQLList<any>,
2120
list: Array<any>,
22-
errors: ReadonlyArray<GraphQLError>,
21+
unpathedErrors: Array<GraphQLError>,
2322
subschema: GraphQLSchema | SubschemaConfig,
2423
context: Record<string, any>,
2524
info: GraphQLResolveInfo,
2625
skipTypeMerging?: boolean
2726
) {
28-
const childErrors = getErrorsByPathSegment(errors);
29-
30-
return list.map((listMember, index) =>
27+
return list.map(listMember =>
3128
handleListMember(
3229
getNullableType(type.ofType),
3330
listMember,
34-
index in childErrors ? childErrors[index] : [],
31+
unpathedErrors,
3532
subschema,
3633
context,
3734
info,
@@ -43,21 +40,25 @@ export function handleList(
4340
function handleListMember(
4441
type: GraphQLType,
4542
listMember: any,
46-
errors: ReadonlyArray<GraphQLError>,
43+
unpathedErrors: Array<GraphQLError>,
4744
subschema: GraphQLSchema | SubschemaConfig,
4845
context: Record<string, any>,
4946
info: GraphQLResolveInfo,
5047
skipTypeMerging?: boolean
5148
): any {
49+
if (listMember instanceof Error) {
50+
return listMember;
51+
}
52+
5253
if (listMember == null) {
53-
return handleNull(errors);
54+
return handleNull(unpathedErrors);
5455
}
5556

5657
if (isLeafType(type)) {
5758
return type.parseValue(listMember);
5859
} else if (isCompositeType(type)) {
59-
return handleObject(type, listMember, errors, subschema, context, info, skipTypeMerging);
60+
return handleObject(type, listMember, unpathedErrors, subschema, context, info, skipTypeMerging);
6061
} else if (isListType(type)) {
61-
return handleList(type, listMember, errors, subschema, context, info, skipTypeMerging);
62+
return handleList(type, listMember, unpathedErrors, subschema, context, info, skipTypeMerging);
6263
}
6364
}

packages/delegate/src/resolveExternalValue/handleNull.ts

+18-19
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,27 @@
1-
import { GraphQLError } from 'graphql';
1+
import { GraphQLError, locatedError } from 'graphql';
22

33
import AggregateError from '@ardatan/aggregate-error';
44

5-
import { relocatedError, unextendedError } from '@graphql-tools/utils';
5+
const reportedErrors: WeakMap<GraphQLError, boolean> = new Map();
66

7-
export function handleNull(errors: ReadonlyArray<GraphQLError>) {
8-
if (errors.length) {
9-
const graphQLToolsMergedPath = errors[0].extensions.graphQLToolsMergedPath;
10-
const unannotatedErrors = errors.map(error => unextendedError(error, 'graphQLToolsMergedPath'));
7+
export function handleNull(unpathedErrors: Array<GraphQLError>) {
8+
if (unpathedErrors.length) {
9+
const unreportedErrors: Array<GraphQLError> = [];
10+
unpathedErrors.forEach(error => {
11+
if (!reportedErrors.has(error)) {
12+
unreportedErrors.push(error);
13+
reportedErrors.set(error, true);
14+
}
15+
});
1116

12-
if (unannotatedErrors.length > 1) {
13-
const combinedError = new AggregateError(unannotatedErrors);
14-
return new GraphQLError(
15-
combinedError.message,
16-
undefined,
17-
undefined,
18-
undefined,
19-
graphQLToolsMergedPath,
20-
combinedError
21-
);
22-
}
17+
if (unreportedErrors.length) {
18+
if (unreportedErrors.length === 1) {
19+
return unreportedErrors[0];
20+
}
2321

24-
const error = unannotatedErrors[0];
25-
return relocatedError(error, graphQLToolsMergedPath);
22+
const combinedError = new AggregateError(unreportedErrors);
23+
return locatedError(combinedError, undefined, unreportedErrors[0].path);
24+
}
2625
}
2726

2827
return null;

packages/delegate/src/resolveExternalValue/handleObject.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { GraphQLCompositeType, GraphQLError, GraphQLSchema, isAbstractType, GraphQLResolveInfo } from 'graphql';
22

3-
import { slicedError } from '@graphql-tools/utils';
4-
53
import { SubschemaConfig } from '../types';
64
import { annotateExternalData } from '../externalData';
75

@@ -11,19 +9,15 @@ import { getFieldsNotInSubschema } from './getFieldsNotInSubschema';
119
export function handleObject(
1210
type: GraphQLCompositeType,
1311
object: any,
14-
errors: ReadonlyArray<GraphQLError>,
12+
unpathedErrors: Array<GraphQLError>,
1513
subschema: GraphQLSchema | SubschemaConfig,
1614
context: Record<string, any>,
1715
info: GraphQLResolveInfo,
1816
skipTypeMerging?: boolean
1917
) {
2018
const stitchingInfo = info?.schema.extensions?.stitchingInfo;
2119

22-
annotateExternalData(
23-
object,
24-
errors.map(error => slicedError(error)),
25-
subschema
26-
);
20+
annotateExternalData(object, unpathedErrors, subschema);
2721

2822
if (skipTypeMerging || !stitchingInfo) {
2923
return object;

packages/delegate/src/resolveExternalValue/index.ts

+8-21
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,38 @@ import {
66
isListType,
77
GraphQLError,
88
GraphQLSchema,
9-
responsePathAsArray,
109
} from 'graphql';
1110

1211
import { SubschemaConfig } from '../types';
1312

1413
import { handleNull } from './handleNull';
1514
import { handleObject } from './handleObject';
1615
import { handleList } from './handleList';
17-
import { extendedError } from '@graphql-tools/utils';
1816

1917
export function resolveExternalValue(
2018
result: any,
21-
errors: ReadonlyArray<GraphQLError>,
19+
unpathedErrors: Array<GraphQLError>,
2220
subschema: GraphQLSchema | SubschemaConfig,
2321
context: Record<string, any>,
2422
info: GraphQLResolveInfo,
2523
returnType = info.returnType,
2624
skipTypeMerging?: boolean
2725
): any {
28-
const annotatedErrors = errors.map(error => {
29-
if (error.extensions?.graphQLToolsMergedPath == null) {
30-
return extendedError(error, {
31-
...error.extensions,
32-
graphQLToolsMergedPath:
33-
info == null
34-
? error.path
35-
: error.path != null
36-
? [...responsePathAsArray(info.path), ...error.path.slice(1)]
37-
: responsePathAsArray(info.path),
38-
});
39-
}
40-
return error;
41-
});
42-
4326
const type = getNullableType(returnType);
4427

28+
if (result instanceof Error) {
29+
return result;
30+
}
31+
4532
if (result == null) {
46-
return handleNull(annotatedErrors);
33+
return handleNull(unpathedErrors);
4734
}
4835

4936
if (isLeafType(type)) {
5037
return type.parseValue(result);
5138
} else if (isCompositeType(type)) {
52-
return handleObject(type, result, annotatedErrors, subschema, context, info, skipTypeMerging);
39+
return handleObject(type, result, unpathedErrors, subschema, context, info, skipTypeMerging);
5340
} else if (isListType(type)) {
54-
return handleList(type, result, annotatedErrors, subschema, context, info, skipTypeMerging);
41+
return handleList(type, result, unpathedErrors, subschema, context, info, skipTypeMerging);
5542
}
5643
}

packages/delegate/src/symbols.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export const ERROR_SYMBOL = Symbol('subschemaErrors');
1+
export const UNPATHED_ERRORS_SYMBOL = Symbol('subschemaErrors');
22
export const OBJECT_SUBSCHEMA_SYMBOL = Symbol('initialSubschema');
33
export const FIELD_SUBSCHEMA_MAP_SYMBOL = Symbol('subschemaMap');

0 commit comments

Comments
 (0)