Skip to content
This repository has been archived by the owner on Apr 15, 2020. It is now read-only.

Commit

Permalink
fix(stitching): improve error proxying for lists
Browse files Browse the repository at this point in the history
required significant refactoring of error handling
  • Loading branch information
yaacovCR committed Dec 9, 2019
1 parent 957e1bc commit 045b515
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 178 deletions.
5 changes: 0 additions & 5 deletions src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,3 @@ export type TypeVisitor = (
type: GraphQLType,
schema: GraphQLSchema,
) => GraphQLNamedType | null | undefined;

export type Path = {
prev: Path;
key: string | number;
};
161 changes: 110 additions & 51 deletions src/stitching/checkResultAndHandleErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ExecutionResult,
GraphQLCompositeType,
GraphQLError,
GraphQLList,
GraphQLType,
GraphQLSchema,
FieldNode,
Expand All @@ -17,14 +18,14 @@ import { getResponseKeyFromInfo } from './getResponseKeyFromInfo';
import {
relocatedError,
combineErrors,
createMergedResult
getErrorsByPathSegment,
} from './errors';
import {
SubschemaConfig,
IGraphQLToolsResolveInfo,
Path,
} from '../Interfaces';
import resolveFromParentTypename from './resolveFromParentTypename';
import { setErrors, setSubschemas } from './proxiedResult';

export function checkResultAndHandleErrors(
result: ExecutionResult,
Expand All @@ -37,90 +38,122 @@ export function checkResultAndHandleErrors(
responseKey = getResponseKeyFromInfo(info);
}

if (!result.data || result.data[responseKey] == null) {
return (result.errors) ? handleErrors(info.fieldNodes, info.path, result.errors) : null;
}
const errors = result.errors || [];
const data = result.data && result.data[responseKey];
const subschemas = [subschema];

return handleResult(
getNullableType(info.returnType),
result.data[responseKey],
result.errors || [],
[subschema],
context,
info,
);
return handleResult(data, errors, subschemas, context, info);
}

export function handleResult(
type: GraphQLType,
result: any,
errors: ReadonlyArray<GraphQLError>,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
): any {
const type = getNullableType(info.returnType);

if (result == null) {
return handleNull(info.fieldNodes, responsePathAsArray(info.path), errors);
}

if (isLeafType(type)) {
return type.parseValue(result);
} else if (isCompositeType(type)) {
return mergeResultsFromOtherSubschemas(
const object = handleObject(result, errors, subschemas);
return mergeFields(
type,
createMergedResult(result, errors, subschemas),
object,
subschemas,
context,
info,
);
} else if (isListType(type)) {
return createMergedResult(result, errors, subschemas).map(
(r: any) => handleListResult(
getNullableType(type.ofType),
r,
subschemas,
context,
info,
)
);
return handleList(type, result, errors, subschemas, info);
}
}

function handleListResult(
export function handleObject (
object: any,
errors: ReadonlyArray<GraphQLError>,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
) {
setErrors(object, errors.map(error => {
return relocatedError(
error,
error.nodes,
error.path ? error.path.slice(1) : undefined
);
}));
setSubschemas(object, subschemas);

return object;
}

function handleList(
type: GraphQLList<any>,
list: Array<any>,
errors: ReadonlyArray<GraphQLError>,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
info: IGraphQLToolsResolveInfo,
) {

const childErrors = getErrorsByPathSegment(errors);

list = list.map((listMember, index) => handleListMember(
getNullableType(type.ofType),
listMember,
index,
childErrors[index] || [],
subschemas,
context,
info,
));

return list;
}

function handleListMember(
type: GraphQLType,
result: any,
listMember: any,
index: number,
errors: ReadonlyArray<GraphQLError>,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
) {
): any {
if (listMember == null) {
return handleNull(info.fieldNodes, [...responsePathAsArray(info.path), index], errors);
}

if (isLeafType(type)) {
return type.parseValue(result);
return type.parseValue(listMember);
} else if (isCompositeType(type)) {
return mergeResultsFromOtherSubschemas(
const object = handleObject(listMember, errors, subschemas);
return mergeFields(
type,
result,
object,
subschemas,
context,
info
);
} else if (isListType(type)) {
return result.map((r: any) => handleListResult(
getNullableType(type.ofType),
r,
subschemas,
context,
info,
));
return handleList(type, listMember, errors, subschemas, info);
}
}

async function mergeResultsFromOtherSubschemas(
async function mergeFields(
type: GraphQLCompositeType,
result: any,
object: any,
subschemas: Array<GraphQLSchema | SubschemaConfig>,
context: Record<string, any>,
info: IGraphQLToolsResolveInfo,
): Promise<any> {
if (info.mergeInfo) {
let typeName: string;
if (isAbstractType(type)) {
typeName = info.schema.getTypeMap()[resolveFromParentTypename(result)].name;
typeName = info.schema.getTypeMap()[resolveFromParentTypename(object)].name;
} else {
typeName = type.name;
}
Expand All @@ -135,30 +168,56 @@ async function mergeResultsFromOtherSubschemas(
if (remainingSubschemas.length) {
const results = await Promise.all(remainingSubschemas.map(subschema => {
const mergedTypeResolver = subschema.mergedTypeConfigs[typeName].mergedTypeResolver;
return mergedTypeResolver(subschema, result, context, {
return mergedTypeResolver(subschema, object, context, {
...info,
mergeInfo: {
...info.mergeInfo,
mergedTypes: {},
},
});
}));
results.forEach((r: ExecutionResult) => Object.assign(result, r));
results.forEach((r: ExecutionResult) => Object.assign(object, r));
}
}
}

return result;
return object;
}

export function handleErrors(
export function handleNull(
fieldNodes: ReadonlyArray<FieldNode>,
path: Path,
path: Array<string | number>,
errors: ReadonlyArray<GraphQLError>,
) {
throw relocatedError(
combineErrors(errors),
fieldNodes,
responsePathAsArray(path)
);
if (errors.length) {
if (errors.some(error => !error.path || error.path.length < 2)) {
return relocatedError(
combineErrors(errors),
fieldNodes,
path,
);

} else if (errors.some(error => typeof error.path[1] === 'string')) {
const childErrors = getErrorsByPathSegment(errors);

const result = Object.create(null);
Object.keys(childErrors).forEach(pathSegment => {
result[pathSegment] = handleNull(fieldNodes, [...path, pathSegment], childErrors[pathSegment]);
});

return result;

} else {
const childErrors = getErrorsByPathSegment(errors);

const result = new Array;
Object.keys(childErrors).forEach(pathSegment => {
result.push(handleNull(fieldNodes, [...path, parseInt(pathSegment, 10)], childErrors[pathSegment]));
});

return result;
}
} else {
return null;
}
}
23 changes: 9 additions & 14 deletions src/stitching/createMergedResolver.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { GraphQLObjectType, getNamedType } from 'graphql';
import { GraphQLObjectType, getNamedType, responsePathAsArray } from 'graphql';
import { IFieldResolver } from '../Interfaces';
import {
getErrorsFromParent,
createMergedResult,
getSubschemasFromParent,
} from './errors';
getErrors,
getSubschemas,
} from './proxiedResult';
import defaultMergedResolver from './defaultMergedResolver';
import { extractOneLevelOfFields } from './extractFields';
import { handleErrors } from './checkResultAndHandleErrors';
import { handleNull, handleObject } from './checkResultAndHandleErrors';

export function wrapField(wrapper: string, fieldName: string): IFieldResolver<any, any> {
return createMergedResolver({ fromPath: [wrapper, fieldName] });
Expand Down Expand Up @@ -54,17 +53,13 @@ export function createMergedResolver({

for (let i = 0; i < fromParentPathLength; i++) {
const responseKey = fromPath[i];
const errors = getErrorsFromParent(parent, responseKey);
const subschemas = getSubschemasFromParent(parent);
const errors = getErrors(parent, responseKey);
const subschemas = getSubschemas(parent);
const result = parent[responseKey];
if (result == null) {
if (errors.length) {
handleErrors(fieldNodes, path, errors);
} else {
return null;
}
return handleNull(fieldNodes, responsePathAsArray(path), errors);
}
parent = createMergedResult(result, errors, subschemas);
parent = handleObject(result, errors, subschemas);
}

fieldName = fromPath[fromPathLength - 1];
Expand Down
16 changes: 6 additions & 10 deletions src/stitching/defaultMergedResolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defaultFieldResolver, getNullableType } from 'graphql';
import { getErrorsFromParent, getSubschemasFromParent, MERGED_NULL_SYMBOL } from './errors';
import { handleResult, handleErrors } from './checkResultAndHandleErrors';
import { defaultFieldResolver } from 'graphql';
import { getErrors, getSubschemas } from './proxiedResult';
import { handleResult } from './checkResultAndHandleErrors';
import { getResponseKeyFromInfo } from './getResponseKeyFromInfo';
import { IGraphQLToolsResolveInfo } from '../Interfaces';

Expand All @@ -19,7 +19,7 @@ export default function defaultMergedResolver(
}

const responseKey = getResponseKeyFromInfo(info);
const errors = getErrorsFromParent(parent, responseKey);
const errors = getErrors(parent, responseKey);

// check to see if parent is not a proxied result, i.e. if parent resolver was manually overwritten
// See https://github.com/apollographql/graphql-tools/issues/967
Expand All @@ -28,11 +28,7 @@ export default function defaultMergedResolver(
}

const result = parent[responseKey];
const subschemas = getSubschemas(parent);

if (result == null || result[MERGED_NULL_SYMBOL]) {
return (errors.length) ? handleErrors(info.fieldNodes, info.path, errors) : null;
}

const parentSubschemas = getSubschemasFromParent(parent);
return handleResult(getNullableType(info.returnType), result, errors, parentSubschemas, context, info);
return handleResult(result, errors, subschemas, context, info);
}
Loading

0 comments on commit 045b515

Please sign in to comment.