Skip to content

Commit 29ead57

Browse files
authored
fix(batch-delegate): proxy batched errors (#2288)
Previously, batch-delegate did not reset the path on errors, so an array index would spuriously end up in the path. The new onLocatedError option to delegateToSchema allows batchDelegateToSchema to remove the array index after the array index is used to properly merge the error into the returned object. Previously, any error returned by the batch loader would result in throwing of a new error => now errors returned by the batch loader are caught and returned as is (cf. https://github.com/graphql/graphql-js/blob/cd273ad136d615b3f2f4c830bd8891c7c5590c30/src/execution/execute.js#L693)
1 parent 302228f commit 29ead57

7 files changed

Lines changed: 91 additions & 7 deletions

File tree

.changeset/seven-dryers-vanish.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-tools/batch-delegate': patch
3+
'@graphql-tools/delegate': patch
4+
---
5+
6+
fix(batch-delegate): proxy batched errors

packages/batch-delegate/src/getLoader.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getNamedType, GraphQLOutputType, GraphQLList, GraphQLSchema } from 'gra
33
import DataLoader from 'dataloader';
44

55
import { delegateToSchema, SubschemaConfig } from '@graphql-tools/delegate';
6+
import { relocatedError } from '@graphql-tools/utils';
67

78
import { BatchDelegateOptions, DataLoaderCache } from './types';
89

@@ -15,10 +16,16 @@ function createBatchFn<K = any>(options: BatchDelegateOptions) {
1516
return async (keys: ReadonlyArray<K>) => {
1617
const results = await delegateToSchema({
1718
returnType: new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType),
19+
onLocatedError: originalError =>
20+
relocatedError(originalError, originalError.path.slice(0, 0).concat(originalError.path.slice(2))),
1821
args: argsFromKeys(keys),
1922
...(lazyOptionsFn == null ? options : lazyOptionsFn(options)),
2023
});
2124

25+
if (results instanceof Error) {
26+
return keys.map(() => results);
27+
}
28+
2229
const values = valuesFromResults == null ? results : valuesFromResults(results, keys);
2330

2431
return Array.isArray(values) ? values : keys.map(() => values);

packages/delegate/src/delegateToSchema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export function delegateRequest({
9292
fieldName,
9393
args,
9494
returnType,
95+
onLocatedError,
9596
context,
9697
transforms = [],
9798
transformedSchema,
@@ -134,6 +135,7 @@ export function delegateRequest({
134135
info,
135136
returnType:
136137
returnType ?? info?.returnType ?? getDelegationReturnType(targetSchema, targetOperation, targetFieldName),
138+
onLocatedError,
137139
transforms: allTransforms,
138140
transformedSchema: transformedSchema ?? (subschemaConfig as Subschema)?.transformedSchema ?? targetSchema,
139141
skipTypeMerging,

packages/delegate/src/mergeFields.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,12 @@ export function mergeFields(
180180
const resultMap: Map<Promise<any> | any, SelectionSetNode> = new Map();
181181
delegationMap.forEach((selectionSet: SelectionSetNode, s: Subschema) => {
182182
const resolver = mergedTypeInfo.resolvers.get(s);
183-
const maybePromise = resolver(object, context, info, s, selectionSet);
184-
resultMap.set(maybePromise, selectionSet);
183+
let maybePromise = resolver(object, context, info, s, selectionSet);
185184
if (isPromise(maybePromise)) {
186185
containsPromises = true;
186+
maybePromise = maybePromise.then(undefined, error => error);
187187
}
188+
resultMap.set(maybePromise, selectionSet);
188189
});
189190

190191
return containsPromises

packages/delegate/src/transforms/CheckResultAndHandleErrors.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export default class CheckResultAndHandleErrors implements Transform {
2727
delegationContext.fieldName,
2828
delegationContext.subschema,
2929
delegationContext.returnType,
30-
delegationContext.skipTypeMerging
30+
delegationContext.skipTypeMerging,
31+
delegationContext.onLocatedError
3132
);
3233
}
3334
}
@@ -39,12 +40,14 @@ export function checkResultAndHandleErrors(
3940
responseKey: string = getResponseKeyFromInfo(info),
4041
subschema?: GraphQLSchema | SubschemaConfig,
4142
returnType: GraphQLOutputType = info.returnType,
42-
skipTypeMerging?: boolean
43+
skipTypeMerging?: boolean,
44+
onLocatedError?: (originalError: GraphQLError) => GraphQLError
4345
): any {
4446
const { data, unpathedErrors } = mergeDataAndErrors(
4547
result.data == null ? undefined : result.data[responseKey],
4648
result.errors == null ? [] : result.errors,
47-
info ? responsePathAsArray(info.path) : undefined
49+
info ? responsePathAsArray(info.path) : undefined,
50+
onLocatedError
4851
);
4952

5053
return resolveExternalValue(data, unpathedErrors, subschema, context, info, returnType, skipTypeMerging);
@@ -54,6 +57,7 @@ export function mergeDataAndErrors(
5457
data: any,
5558
errors: ReadonlyArray<GraphQLError>,
5659
path: Array<string | number>,
60+
onLocatedError: (originalError: GraphQLError) => GraphQLError,
5761
index = 1
5862
): { data: any; unpathedErrors: Array<GraphQLError> } {
5963
if (data == null) {
@@ -62,13 +66,16 @@ export function mergeDataAndErrors(
6266
}
6367

6468
if (errors.length === 1) {
65-
const error = errors[0];
69+
const error = onLocatedError ? onLocatedError(errors[0]) : errors[0];
6670
const newPath =
6771
path === undefined ? error.path : error.path === undefined ? path : path.concat(error.path.slice(1));
72+
6873
return { data: relocatedError(errors[0], newPath), unpathedErrors: [] };
6974
}
7075

71-
return { data: locatedError(new AggregateError(errors), undefined, path), unpathedErrors: [] };
76+
const newError = locatedError(new AggregateError(errors), undefined, path);
77+
78+
return { data: newError, unpathedErrors: [] };
7279
}
7380

7481
if (!errors.length) {
@@ -98,6 +105,7 @@ export function mergeDataAndErrors(
98105
data[pathSegment],
99106
errorMap[pathSegment],
100107
path,
108+
onLocatedError,
101109
index + 1
102110
);
103111
data[pathSegment] = newData;

packages/delegate/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface DelegationContext {
5151
context: Record<string, any>;
5252
info: GraphQLResolveInfo;
5353
returnType: GraphQLOutputType;
54+
onLocatedError?: (originalError: GraphQLError) => GraphQLError;
5455
transforms: Array<Transform>;
5556
transformedSchema: GraphQLSchema;
5657
skipTypeMerging: boolean;
@@ -64,6 +65,7 @@ export interface IDelegateToSchemaOptions<TContext = Record<string, any>, TArgs
6465
operation?: OperationTypeNode;
6566
fieldName?: string;
6667
returnType?: GraphQLOutputType;
68+
onLocatedError?: (originalError: GraphQLError) => GraphQLError;
6769
args?: TArgs;
6870
selectionSet?: SelectionSetNode;
6971
fieldNodes?: ReadonlyArray<FieldNode>;

packages/stitch/tests/mergeFailures.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,64 @@ describe('merge failures', () => {
8585
expect(result).toEqual(expectedResult);
8686
});
8787

88+
test('proxies merged error arrays', async () => {
89+
const schema1 = makeExecutableSchema({
90+
typeDefs: `
91+
type Thing {
92+
id: ID!
93+
name: String
94+
desc: String
95+
}
96+
type Query {
97+
things(ids: [ID!]!): [Thing]!
98+
}
99+
`,
100+
resolvers: {
101+
Query: {
102+
things: () => [new Error('no thing')],
103+
},
104+
},
105+
});
106+
107+
const schema2 = makeExecutableSchema({
108+
typeDefs: `
109+
type ParentThing {
110+
thing: Thing
111+
}
112+
type Thing {
113+
id: ID!
114+
}
115+
type Query {
116+
parent: ParentThing
117+
}
118+
`,
119+
resolvers: {
120+
Query: {
121+
parent: () => ({ thing: { id: 23 } }),
122+
},
123+
},
124+
});
125+
126+
const stitchedSchema = stitchSchemas({
127+
subschemas: [{
128+
schema: schema1,
129+
merge: {
130+
Thing: {
131+
selectionSet: '{ id }',
132+
fieldName: 'things',
133+
key: ({ id }) => id,
134+
argsFromKeys: (ids) => ({ ids }),
135+
},
136+
}
137+
}, {
138+
schema: schema2,
139+
}],
140+
});
141+
142+
const stitchedResult = await graphql(stitchedSchema, '{ parent { thing { name desc id } } }');
143+
expect(stitchedResult.errors[0].path).toEqual(['parent', 'thing', 'name']);
144+
});
145+
88146
it('proxies inappropriate null', async () => {
89147
const secondSchema = makeExecutableSchema({
90148
typeDefs: `

0 commit comments

Comments
 (0)