Skip to content

Commit

Permalink
Handle errors better in merged schemas (#425)
Browse files Browse the repository at this point in the history
* Translate paths
* Throw errors per field with errors from remote schema
  • Loading branch information
freiksenet committed Oct 11, 2017
1 parent 6793c0e commit 3e4b61a
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 119 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

### VNEXT

* Translate errors better in merged schema [Issue #419]((https://github.com/apollographql/graphql-tools/issues/419) [PR #425](https://github.com/apollographql/graphql-tools/pull/425)

### v2.3.0

* Fix alias issues [Issue #415](https://github.com/apollographql/graphql-tools/issues/415) [PR #418](https://github.com/apollographql/graphql-tools/pull/418)
Expand Down
19 changes: 0 additions & 19 deletions src/stitching/aliasAwareResolver.ts

This file was deleted.

35 changes: 35 additions & 0 deletions src/stitching/defaultMergedResolver.ts
@@ -0,0 +1,35 @@
import { GraphQLFieldResolver, responsePathAsArray } from 'graphql';
import { locatedError } from 'graphql/error';
import { getErrorsFromParent, annotateWithChildrenErrors } from './errors';

// Resolver that knows how to:
// a) handle aliases for proxied schemas
// b) handle errors from proxied schemas
const defaultMergedResolver: GraphQLFieldResolver<any, any> = (
parent,
args,
context,
info,
) => {
const responseKey = info.fieldNodes[0].alias
? info.fieldNodes[0].alias.value
: info.fieldName;
const errorResult = getErrorsFromParent(parent, responseKey);
if (errorResult.kind === 'OWN') {
throw locatedError(
errorResult.error.message,
info.fieldNodes,
responsePathAsArray(info.path),
);
} else if (parent) {
let result = parent[responseKey];
if (errorResult.errors) {
result = annotateWithChildrenErrors(result, errorResult.errors);
}
return result;
} else {
return null;
}
};

export default defaultMergedResolver;
98 changes: 98 additions & 0 deletions src/stitching/errors.ts
@@ -0,0 +1,98 @@
import { GraphQLResolveInfo, responsePathAsArray } from 'graphql';
import { locatedError } from 'graphql/error';

const ERROR_SYMBOL = Symbol('subSchemaErrors');

export function annotateWithChildrenErrors(
object: any,
childrenErrors: Array<{ path?: Array<string | number> }>,
): any {
if (childrenErrors && childrenErrors.length > 0) {
if (Array.isArray(object)) {
const byIndex = {};
childrenErrors.forEach(error => {
const index = error.path[1];
const current = byIndex[index] || [];
current.push({
...error,
path: error.path.slice(1),
});
byIndex[index] = current;
});
return object.map((item, index) =>
annotateWithChildrenErrors(item, byIndex[index]),
);
} else {
return {
...object,
[ERROR_SYMBOL]: childrenErrors.map(error => ({
...error,
path: error.path.slice(1),
})),
};
}
} else {
return object;
}
}

export function getErrorsFromParent(
object: any,
fieldName: string,
):
| {
kind: 'OWN';
error: any;
}
| {
kind: 'CHILDREN';
errors?: Array<{ path?: Array<string | number> }>;
} {
const errors = (object && object[ERROR_SYMBOL]) || [];
const childrenErrors: Array<{ path?: Array<string | number> }> = [];
for (const error of errors) {
if (error.path.length === 1 && error.path[0] === fieldName) {
return {
kind: 'OWN',
error,
};
} else if (error.path[0] === fieldName) {
childrenErrors.push(error);
}
}
return {
kind: 'CHILDREN',
errors: childrenErrors,
};
}

export function checkResultAndHandleErrors(
result: any,
info: GraphQLResolveInfo,
responseKey?: string,
): any {
if (!responseKey) {
responseKey = info.fieldNodes[0].alias
? info.fieldNodes[0].alias.value
: info.fieldName;
}
if (result.errors && (!result.data || result.data[responseKey] == null)) {
const errorMessage = result.errors
.map((error: { message: string }) => error.message)
.join('\n');
throw locatedError(
errorMessage,
info.fieldNodes,
responsePathAsArray(info.path),
);
} else {
let resultObject = result.data[responseKey];
if (result.errors) {
resultObject = annotateWithChildrenErrors(
resultObject,
result.errors as Array<{ path?: Array<string> }>,
);
}
return resultObject;
}
}
15 changes: 4 additions & 11 deletions src/stitching/makeRemoteExecutableSchema.ts
Expand Up @@ -18,7 +18,8 @@ import isEmptyObject from '../isEmptyObject';
import { IResolvers, IResolverObject } from '../Interfaces';
import { makeExecutableSchema } from '../schemaGenerator';
import resolveParentFromTypename from './resolveFromParentTypename';
import aliasAwareResolver from './aliasAwareResolver';
import defaultMergedResolver from './defaultMergedResolver';
import { checkResultAndHandleErrors } from './errors';

export type Fetcher = (
operation: {
Expand Down Expand Up @@ -115,7 +116,7 @@ export default function makeRemoteExecutableSchema({
) {
const resolver = {};
Object.keys(type.getFields()).forEach(field => {
resolver[field] = aliasAwareResolver;
resolver[field] = defaultMergedResolver;
});
resolvers[type.name] = resolver;
}
Expand Down Expand Up @@ -145,15 +146,7 @@ function createResolver(link: ApolloLink): GraphQLFieldResolver<any, any> {
context: { graphqlContext: context },
}),
);
const fieldName = info.fieldNodes[0].alias
? info.fieldNodes[0].alias.value
: info.fieldName;
if (result.errors) {
const errorMessage = result.errors.map(error => error.message).join('\n');
throw new Error(errorMessage);
} else {
return result.data[fieldName];
}
return checkResultAndHandleErrors(result, info);
};
}

Expand Down
22 changes: 4 additions & 18 deletions src/stitching/mergeSchemas.ts
Expand Up @@ -53,7 +53,8 @@ import {
addResolveFunctionsToSchema,
} from '../schemaGenerator';
import resolveFromParentTypename from './resolveFromParentTypename';
import aliasAwareResolver from './aliasAwareResolver';
import defaultMergedResolver from './defaultMergedResolver';
import { checkResultAndHandleErrors } from './errors';

export type MergeInfo = {
delegate: (
Expand Down Expand Up @@ -290,7 +291,7 @@ function fieldToFieldConfig(
return {
type: registry.resolveType(field.type),
args: argsToFieldConfigArgumentMap(field.args, registry),
resolve: aliasAwareResolver,
resolve: defaultMergedResolver,
description: field.description,
deprecationReason: field.deprecationReason,
};
Expand Down Expand Up @@ -440,22 +441,7 @@ async function delegateToSchema(
variableValues,
);

// const print = require('graphql').print;
// console.log(
// 'RESULT FROM FORWARDING\n',
// print(graphqlDoc),
// '\n',
// JSON.stringify(variableValues, null, 2),
// '\n',
// JSON.stringify(result, null, 2),
// );

if (result.errors) {
const errorMessage = result.errors.map(error => error.message).join('\n');
throw new Error(errorMessage);
} else {
return result.data[fieldName];
}
return checkResultAndHandleErrors(result, info, fieldName);
}

throw new Error('Could not forward to merged schema');
Expand Down

0 comments on commit 3e4b61a

Please sign in to comment.