From ef7e62a6490323f8c8c647e14615f50d5c997925 Mon Sep 17 00:00:00 2001 From: Hugh Willson Date: Fri, 8 Mar 2019 21:28:38 -0500 Subject: [PATCH 1/3] Preserve source error extensions when merging schemas Currently, when merging multiple schemas together, custom error `extensions` (like custom errors `code`'s, custom error properties, etc.) are dropped when errors are returned from child resolvers. This is caused by the current schema stitching error handling code creating copies of errors before reporting them, but not including all properties of the original error in the copy. This commit adjusts the schema stitching error handling code to preserve the `originalError` details when creating error copies. This helps `mergeSchemas` (and other parts of `graphql-tools`) include all source error `extensions`, when reporting errors through the merged schema. --- src/stitching/defaultMergedResolver.ts | 6 ++++- src/stitching/errors.ts | 17 +++++++++----- src/test/testMergeSchemas.ts | 32 ++++++++++++++++++++++++++ src/test/testingSchemas.ts | 6 ++++- 4 files changed, 53 insertions(+), 8 deletions(-) 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 1691fba5997..1a004f8dc3e 100644 --- a/src/test/testMergeSchemas.ts +++ b/src/test/testMergeSchemas.ts @@ -2274,6 +2274,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; }, }, }; From 7fa081e334d9bf7d73fe0f8abb7a627826e36beb Mon Sep 17 00:00:00 2001 From: Hugh Willson Date: Sat, 9 Mar 2019 10:48:41 -0500 Subject: [PATCH 2/3] Prep for RC release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e3c7aa8be2..9a65ef8a047 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-tools", - "version": "4.0.4", + "version": "5.0.0-rc.0", "description": "Useful tools to create and manipulate GraphQL schemas.", "main": "dist/index.js", "typings": "dist/index.d.ts", From bd94cdc58bb94b87c3ad5ab4f9a0be1d2d0f8a60 Mon Sep 17 00:00:00 2001 From: Hugh Willson Date: Tue, 26 Mar 2019 15:45:31 -0400 Subject: [PATCH 3/3] Bump version, prep for `next` publish --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a65ef8a047..f7d7bec0833 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-tools", - "version": "5.0.0-rc.0", + "version": "5.0.0-rc.1", "description": "Useful tools to create and manipulate GraphQL schemas.", "main": "dist/index.js", "typings": "dist/index.d.ts",