Skip to content

Commit a1cf760

Browse files
committed
inital changes for re-attempt at #1047
1 parent 4cadaf7 commit a1cf760

File tree

8 files changed

+114
-71
lines changed

8 files changed

+114
-71
lines changed

packages/delegate/src/proxiedResult.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { GraphQLError } from 'graphql';
2-
3-
import { mergeDeep, ERROR_SYMBOL, relocatedError, setErrors, getErrors } from '@graphql-tools/utils';
1+
import {
2+
mergeDeep,
3+
ERROR_SYMBOL,
4+
relocatedError,
5+
setErrors,
6+
getErrors,
7+
sliceRelativeError,
8+
RelativeGraphQLError,
9+
} from '@graphql-tools/utils';
410

511
import { handleNull } from './results/handleNull';
612

@@ -27,7 +33,7 @@ export function unwrapResult(parent: any, path: Array<string>): any {
2733

2834
setErrors(
2935
object,
30-
errors.map(error => relocatedError(error, error.path != null ? error.path.slice(1) : undefined))
36+
errors.map(error => sliceRelativeError(error))
3137
);
3238
setObjectSubschema(object, subschema);
3339

@@ -51,15 +57,15 @@ export function dehoistResult(parent: any, delimeter = '__gqltf__'): any {
5157
obj[fieldName] = parent[alias];
5258
});
5359

54-
result[ERROR_SYMBOL] = parent[ERROR_SYMBOL].map((error: GraphQLError) => {
55-
if (error.path != null) {
56-
const path = error.path.slice();
57-
const pathSegment = path.shift();
58-
const expandedPathSegment: Array<string | number> = (pathSegment as string).split(delimeter);
59-
return relocatedError(error, expandedPathSegment.concat(path));
60-
}
61-
62-
return error;
60+
result[ERROR_SYMBOL] = parent[ERROR_SYMBOL].map((error: RelativeGraphQLError) => {
61+
const path = error.relativePath.slice();
62+
const pathSegment = path.shift();
63+
const expandedPathSegment: Array<string | number> = (pathSegment as string).split(delimeter);
64+
return {
65+
relativePath: expandedPathSegment.concat(path),
66+
// setting path to null will cause issues for errors that bubble up from non nullable fields
67+
graphQLError: relocatedError(error.graphQLError, null),
68+
};
6369
});
6470

6571
result[OBJECT_SUBSCHEMA_SYMBOL] = parent[OBJECT_SUBSCHEMA_SYMBOL];

packages/delegate/src/results/handleList.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
GraphQLList,
33
GraphQLSchema,
4-
GraphQLError,
54
GraphQLResolveInfo,
65
getNullableType,
76
GraphQLType,
@@ -10,7 +9,7 @@ import {
109
isListType,
1110
} from 'graphql';
1211

13-
import { getErrorsByPathSegment } from '@graphql-tools/utils';
12+
import { getErrorsByPathSegment, RelativeGraphQLError } from '@graphql-tools/utils';
1413

1514
import { handleNull } from './handleNull';
1615
import { handleObject } from './handleObject';
@@ -19,7 +18,7 @@ import { SubschemaConfig } from '../types';
1918
export function handleList(
2019
type: GraphQLList<any>,
2120
list: Array<any>,
22-
errors: ReadonlyArray<GraphQLError>,
21+
errors: Array<RelativeGraphQLError>,
2322
subschema: GraphQLSchema | SubschemaConfig,
2423
context: Record<string, any>,
2524
info: GraphQLResolveInfo,
@@ -43,7 +42,7 @@ export function handleList(
4342
function handleListMember(
4443
type: GraphQLType,
4544
listMember: any,
46-
errors: ReadonlyArray<GraphQLError>,
45+
errors: Array<RelativeGraphQLError>,
4746
subschema: GraphQLSchema | SubschemaConfig,
4847
context: Record<string, any>,
4948
info: GraphQLResolveInfo,

packages/delegate/src/results/handleNull.ts

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,13 @@
1-
import { GraphQLError } from 'graphql';
2-
31
import AggregateError from 'aggregate-error';
4-
import { getErrorsByPathSegment, relocatedError } from '@graphql-tools/utils';
2+
import { RelativeGraphQLError } from '@graphql-tools/utils';
53

6-
export function handleNull(errors: ReadonlyArray<GraphQLError>) {
4+
export function handleNull(errors: Array<RelativeGraphQLError>) {
75
if (errors.length) {
8-
if (errors.some(error => !error.path || error.path.length < 2)) {
9-
if (errors.length > 1) {
10-
const combinedError = new AggregateError(errors);
11-
return combinedError;
12-
}
13-
const error = errors[0];
14-
return error.originalError || relocatedError(error, null);
15-
} else if (errors.some(error => typeof error.path[1] === 'string')) {
16-
const childErrors = getErrorsByPathSegment(errors);
17-
18-
const result = {};
19-
Object.keys(childErrors).forEach(pathSegment => {
20-
result[pathSegment] = handleNull(childErrors[pathSegment]);
21-
});
22-
23-
return result;
6+
if (errors.length > 1) {
7+
return new AggregateError(errors.map(error => error.graphQLError));
248
}
259

26-
const childErrors = getErrorsByPathSegment(errors);
27-
28-
const result: Array<any> = [];
29-
Object.keys(childErrors).forEach(pathSegment => {
30-
result.push(handleNull(childErrors[pathSegment]));
31-
});
32-
33-
return result;
10+
return errors[0].graphQLError;
3411
}
3512

3613
return null;

packages/delegate/src/results/handleObject.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
import {
22
GraphQLCompositeType,
3-
GraphQLError,
43
GraphQLSchema,
54
isAbstractType,
65
FieldNode,
76
GraphQLObjectType,
87
GraphQLResolveInfo,
98
} from 'graphql';
109

11-
import { collectFields, GraphQLExecutionContext, setErrors, slicedError } from '@graphql-tools/utils';
10+
import {
11+
collectFields,
12+
GraphQLExecutionContext,
13+
setErrors,
14+
RelativeGraphQLError,
15+
sliceRelativeError,
16+
} from '@graphql-tools/utils';
1217
import { setObjectSubschema, isSubschemaConfig } from '../Subschema';
1318
import { mergeFields } from '../mergeFields';
1419
import { MergedTypeInfo, SubschemaConfig } from '../types';
1520

1621
export function handleObject(
1722
type: GraphQLCompositeType,
1823
object: any,
19-
errors: ReadonlyArray<GraphQLError>,
24+
errors: Array<RelativeGraphQLError>,
2025
subschema: GraphQLSchema | SubschemaConfig,
2126
context: Record<string, any>,
2227
info: GraphQLResolveInfo,
@@ -26,7 +31,7 @@ export function handleObject(
2631

2732
setErrors(
2833
object,
29-
errors.map(error => slicedError(error))
34+
errors.map(error => sliceRelativeError(error))
3035
);
3136

3237
setObjectSubschema(object, subschema);

packages/delegate/src/results/handleResult.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import {
2-
GraphQLResolveInfo,
3-
getNullableType,
4-
isCompositeType,
5-
isLeafType,
6-
isListType,
7-
GraphQLError,
8-
GraphQLSchema,
9-
} from 'graphql';
1+
import { GraphQLResolveInfo, getNullableType, isCompositeType, isLeafType, isListType, GraphQLSchema } from 'graphql';
2+
3+
import { RelativeGraphQLError } from '@graphql-tools/utils';
104

115
import { SubschemaConfig } from '../types';
126

@@ -16,7 +10,7 @@ import { handleList } from './handleList';
1610

1711
export function handleResult(
1812
result: any,
19-
errors: ReadonlyArray<GraphQLError>,
13+
errors: Array<RelativeGraphQLError>,
2014
subschema: GraphQLSchema | SubschemaConfig,
2115
context: Record<string, any>,
2216
info: GraphQLResolveInfo,

packages/delegate/src/transforms/CheckResultAndHandleErrors.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GraphQLResolveInfo, ExecutionResult, GraphQLOutputType, GraphQLSchema } from 'graphql';
22

3-
import { Transform, getResponseKeyFromInfo } from '@graphql-tools/utils';
3+
import { Transform, getResponseKeyFromInfo, RelativeGraphQLError, toRelativeError } from '@graphql-tools/utils';
44
import { handleResult } from '../results/handleResult';
55
import { SubschemaConfig } from '../types';
66

@@ -50,7 +50,8 @@ export function checkResultAndHandleErrors(
5050
returnType: GraphQLOutputType = info.returnType,
5151
skipTypeMerging?: boolean
5252
): any {
53-
const errors = result.errors != null ? result.errors : [];
53+
const errors: Array<RelativeGraphQLError> =
54+
result.errors != null ? result.errors.map(error => toRelativeError(error, info)) : [];
5455
const data = result.data != null ? result.data[responseKey] : undefined;
5556

5657
return handleResult(data, errors, subschema, context, info, returnType, skipTypeMerging);

packages/stitch/tests/errors.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe('passes along errors for missing fields on list', () => {
3838
const originalResult = await graphql(schema, query);
3939
const stitchedResult = await graphql(stitchedSchema, query);
4040
expect(stitchedResult).toEqual(originalResult);
41+
expect(stitchedResult.errors[0].path).toEqual(originalResult.errors[0].path);
4142
});
4243

4344
test('even if nullable', async () => {
@@ -72,6 +73,7 @@ describe('passes along errors for missing fields on list', () => {
7273
const originalResult = await graphql(schema, query);
7374
const stitchedResult = await graphql(stitchedSchema, query);
7475
expect(stitchedResult).toEqual(originalResult);
76+
expect(stitchedResult.errors[0].path).toEqual(originalResult.errors[0].path);
7577
});
7678
});
7779

@@ -108,6 +110,7 @@ describe('passes along errors when list field errors', () => {
108110
const originalResult = await graphql(schema, query);
109111
const stitchedResult = await graphql(stitchedSchema, query);
110112
expect(stitchedResult).toEqual(originalResult);
113+
expect(stitchedResult.errors[0].path).toEqual(originalResult.errors[0].path);
111114
});
112115

113116
test('even if nullable', async () => {
@@ -142,6 +145,38 @@ describe('passes along errors when list field errors', () => {
142145
const originalResult = await graphql(schema, query);
143146
const stitchedResult = await graphql(stitchedSchema, query);
144147
expect(stitchedResult).toEqual(originalResult);
148+
expect(stitchedResult.errors[0].path).toEqual(originalResult.errors[0].path);
149+
});
150+
151+
describe('passes along correct error when there are two non-null fields', () => {
152+
test('should work', async () => {
153+
const schema = makeExecutableSchema({
154+
typeDefs: `
155+
type Query {
156+
getBoth: Both
157+
}
158+
type Both {
159+
mandatoryField1: String!
160+
mandatoryField2: String!
161+
}
162+
`,
163+
resolvers: {
164+
Query: {
165+
getBoth: () => ({ mandatoryField1: 'test' }),
166+
},
167+
},
168+
});
169+
170+
const stitchedSchema = stitchSchemas({
171+
subschemas: [schema],
172+
});
173+
174+
const query = '{ getBoth { mandatoryField1 mandatoryField2 } }';
175+
const originalResult = await graphql(schema, query);
176+
const stitchedResult = await graphql(stitchedSchema, query);
177+
expect(stitchedResult).toEqual(originalResult);
178+
expect(stitchedResult.errors[0].path).toEqual(originalResult.errors[0].path);
179+
});
145180
});
146181
});
147182

packages/utils/src/errors.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import { GraphQLError } from 'graphql';
1+
import { GraphQLError, responsePathAsArray, GraphQLResolveInfo } from 'graphql';
22

33
export const ERROR_SYMBOL = Symbol('subschemaErrors');
44

5+
export interface RelativeGraphQLError {
6+
relativePath: Array<string | number>;
7+
graphQLError: GraphQLError;
8+
}
9+
510
export function relocatedError(originalError: GraphQLError, path?: ReadonlyArray<string | number>): GraphQLError {
611
return new GraphQLError(
712
originalError.message,
@@ -18,28 +23,49 @@ export function slicedError(originalError: GraphQLError) {
1823
return relocatedError(originalError, originalError.path != null ? originalError.path.slice(1) : undefined);
1924
}
2025

21-
export function getErrorsByPathSegment(errors: ReadonlyArray<GraphQLError>): Record<string, Array<GraphQLError>> {
22-
const record = Object.create(null);
26+
export function getErrorsByPathSegment(
27+
errors: Array<RelativeGraphQLError>
28+
): Record<string, Array<RelativeGraphQLError>> {
29+
const record: Record<string, Array<RelativeGraphQLError>> = Object.create(null);
2330
errors.forEach(error => {
24-
if (!error.path || error.path.length < 2) {
31+
if (!error.relativePath || error.relativePath.length < 2) {
2532
return;
2633
}
2734

28-
const pathSegment = error.path[1];
35+
const pathSegment = error.relativePath[1];
2936

30-
const current = pathSegment in record ? record[pathSegment] : [];
31-
current.push(slicedError(error));
37+
const current: Array<RelativeGraphQLError> = pathSegment in record ? record[pathSegment] : [];
38+
current.push({
39+
relativePath: error.relativePath.slice(1),
40+
graphQLError: error.graphQLError,
41+
});
3242
record[pathSegment] = current;
3343
});
3444

3545
return record;
3646
}
3747

38-
export function setErrors(result: any, errors: Array<GraphQLError>) {
48+
export function toRelativeError(error: Readonly<GraphQLError>, info: GraphQLResolveInfo): RelativeGraphQLError {
49+
const relativePath = error.path?.slice() || [];
50+
const sourcePath = info != null ? responsePathAsArray(info.path) : [];
51+
return {
52+
relativePath,
53+
graphQLError: relocatedError(error, sourcePath.concat(relativePath.slice(1))),
54+
};
55+
}
56+
57+
export function sliceRelativeError(error: RelativeGraphQLError): RelativeGraphQLError {
58+
return {
59+
...error,
60+
relativePath: error.relativePath.slice(1),
61+
};
62+
}
63+
64+
export function setErrors(result: any, errors: Array<RelativeGraphQLError>) {
3965
result[ERROR_SYMBOL] = errors;
4066
}
4167

42-
export function getErrors(result: any, pathSegment: string): Array<GraphQLError> {
68+
export function getErrors(result: any, pathSegment: string): Array<RelativeGraphQLError> {
4369
const errors = result != null ? result[ERROR_SYMBOL] : result;
4470

4571
if (!Array.isArray(errors)) {
@@ -49,7 +75,7 @@ export function getErrors(result: any, pathSegment: string): Array<GraphQLError>
4975
const fieldErrors = [];
5076

5177
for (const error of errors) {
52-
if (!error.path || error.path[0] === pathSegment) {
78+
if (!error.relativePath || error.relativePath[0] === pathSegment) {
5379
fieldErrors.push(error);
5480
}
5581
}

0 commit comments

Comments
 (0)