diff --git a/package.json b/package.json index 2e3c7aa8be2..f7d7bec0833 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-tools", - "version": "4.0.4", + "version": "5.0.0-rc.1", "description": "Useful tools to create and manipulate GraphQL schemas.", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/stitching/defaultMergedResolver.ts b/src/stitching/defaultMergedResolver.ts index cbf754a03a8..6e74611d8b9 100644 --- a/src/stitching/defaultMergedResolver.ts +++ b/src/stitching/defaultMergedResolver.ts @@ -15,7 +15,11 @@ const defaultMergedResolver: GraphQLFieldResolver = (parent, args, con const errorResult = getErrorsFromParent(parent, responseKey); if (errorResult.kind === 'OWN') { - throw locatedError(new Error(errorResult.error.message), info.fieldNodes, responsePathAsArray(info.path)); + throw locatedError( + errorResult.error.originalError || new Error(errorResult.error.message), + info.fieldNodes, + responsePathAsArray(info.path), + ); } let result = parent[responseKey]; diff --git a/src/stitching/errors.ts b/src/stitching/errors.ts index 94ba6851a25..289d8929c73 100644 --- a/src/stitching/errors.ts +++ b/src/stitching/errors.ts @@ -2,7 +2,6 @@ import { GraphQLResolveInfo, responsePathAsArray, ExecutionResult, - GraphQLFormattedError, GraphQLError, } from 'graphql'; import { locatedError } from 'graphql/error'; @@ -18,7 +17,7 @@ if ( ERROR_SYMBOL = '@@__subSchemaErrors'; } -export function annotateWithChildrenErrors(object: any, childrenErrors: ReadonlyArray): any { +export function annotateWithChildrenErrors(object: any, childrenErrors: ReadonlyArray): any { if (!childrenErrors || childrenErrors.length === 0) { // Nothing to see here, move along return object; @@ -33,10 +32,16 @@ export function annotateWithChildrenErrors(object: any, childrenErrors: Readonly } const index = error.path[1]; const current = byIndex[index] || []; + + // It's important to keep the `originalError`, to make sure + // error stacktrace's and custom `extensions` are preserved from + // source schemas, when merged. current.push({ ...error, - path: error.path.slice(1) + path: error.path.slice(1), + originalError: error.originalError, }); + byIndex[index] = current; }); @@ -62,10 +67,10 @@ export function getErrorsFromParent( } | { kind: 'CHILDREN'; - errors?: Array; + errors?: Array; } { const errors = (object && object[ERROR_SYMBOL]) || []; - const childrenErrors: Array = []; + const childrenErrors: Array = []; for (const error of errors) { if (!error.path || (error.path.length === 1 && error.path[0] === fieldName)) { @@ -114,7 +119,7 @@ export function checkResultAndHandleErrors( let resultObject = result.data[responseKey]; if (result.errors) { - resultObject = annotateWithChildrenErrors(resultObject, result.errors as ReadonlyArray); + resultObject = annotateWithChildrenErrors(resultObject, result.errors as ReadonlyArray); } return resultObject; } diff --git a/src/test/testMergeSchemas.ts b/src/test/testMergeSchemas.ts index 184f57cfca8..7e93104fece 100644 --- a/src/test/testMergeSchemas.ts +++ b/src/test/testMergeSchemas.ts @@ -2315,6 +2315,38 @@ fragment BookingFragment on Booking { ], }); }); + + it( + 'should preserve custom error extensions from the original schema, ' + + 'when merging schemas', + async () => { + const propertyQuery = ` + query { + properties(limit: 1) { + error + } + } + `; + + const propertyResult = await graphql( + propertySchema, + propertyQuery, + ); + + const mergedResult = await graphql( + mergedSchema, + propertyQuery, + ); + + [propertyResult, mergedResult].forEach((result) => { + expect(result.errors).to.exist; + expect(result.errors.length > 0).to.be.true; + const error = result.errors[0]; + expect(error.extensions).to.exist; + expect(error.extensions.code).to.equal('SOME_CUSTOM_CODE'); + }); + } + ); }); describe('types in schema extensions', () => { diff --git a/src/test/testingSchemas.ts b/src/test/testingSchemas.ts index 79c455d43be..b08bb474b79 100644 --- a/src/test/testingSchemas.ts +++ b/src/test/testingSchemas.ts @@ -407,7 +407,11 @@ const propertyResolvers: IResolvers = { Property: { error() { - throw new Error('Property.error error'); + const error = new Error('Property.error error'); + (error as any).extensions = { + code: 'SOME_CUSTOM_CODE', + }; + throw error; }, }, };