diff --git a/src/stitching/createMergedResolver.ts b/src/stitching/createMergedResolver.ts index 5a38b648098..f0062cf9498 100644 --- a/src/stitching/createMergedResolver.ts +++ b/src/stitching/createMergedResolver.ts @@ -1,6 +1,12 @@ +import { GraphQLObjectType, getNamedType, responsePathAsArray } from 'graphql'; import { IFieldResolver } from '../Interfaces'; -import { defaultMergedResolver } from '../stitching'; -import { GraphQLObjectType } from 'graphql'; +import { + relocatedError, + combineErrors, + getErrorsFromParent, + annotateWithChildrenErrors, +} from './errors'; +import defaultMergedResolver from './defaultMergedResolver'; import { extractOneLevelOfFields } from './extractFields'; export function wrapField(wrapper: string, fieldName: string): IFieldResolver { @@ -31,8 +37,8 @@ export function createMergedResolver({ toPath.forEach(pathSegment => { fieldNodes = extractOneLevelOfFields(fieldNodes, pathSegment, info.fragments); - parentType = returnType as GraphQLObjectType; - returnType = (returnType as GraphQLObjectType).getFields()[pathSegment].type; + parentType = getNamedType(returnType) as GraphQLObjectType; + returnType = (parentType as GraphQLObjectType).getFields()[pathSegment].type; path = { prev: path, key: pathSegment }; }); @@ -44,7 +50,27 @@ export function createMergedResolver({ const fromPathLength = fromPath.length; if (fromPathLength) { - parent = fromPath.slice(0, -1).reduce((p, pathSegment) => p[pathSegment], parent); + const fromParentPathLength = fromPathLength - 1; + + for (let i = 0; i < fromParentPathLength; i++) { + const responseKey = fromPath[i]; + const errors = getErrorsFromParent(parent, responseKey); + const result = parent[responseKey]; + if (result == null) { + if (errors.length) { + throw relocatedError( + combineErrors(errors), + fieldNodes, + responsePathAsArray(path) + ); + } else { + return null; + } + } + annotateWithChildrenErrors(result, errors); + parent = result; + } + fieldName = fromPath[fromPathLength - 1]; } diff --git a/src/test/testAlternateMergeSchemas.ts b/src/test/testAlternateMergeSchemas.ts index 05392dccd74..641a50a5926 100644 --- a/src/test/testAlternateMergeSchemas.ts +++ b/src/test/testAlternateMergeSchemas.ts @@ -12,7 +12,7 @@ import { GraphQLScalarType, FieldNode, printSchema, - ExecutableDefinitionNode, + Kind, } from 'graphql'; import { transformSchema, @@ -46,9 +46,35 @@ import { } from '../stitching'; import { SchemaExecutionConfig } from '../Interfaces'; -const toFieldNode = - (raw: string) => - ((parse(`{ ${raw} }`).definitions[0] as ExecutableDefinitionNode).selectionSet.selections[0]); +function renameFieldNode(fieldNode: FieldNode, name: string): FieldNode { + return { + ...fieldNode, + name: { + ...fieldNode.name, + value: name, + } + }; +} + +function wrapFieldNode(fieldNode: FieldNode, path: Array): FieldNode { + let newFieldNode = fieldNode; + path.forEach(fieldName => { + newFieldNode = { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: fieldName, + }, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + fieldNode, + ] + } + }; + }); + return newFieldNode; +} let linkSchema = ` """ @@ -581,6 +607,7 @@ describe('schema transformation with extraction of nested fields', () => { extend type Property { locationName: String locationName2: String + pseudoWrappedError: String } `, resolvers: { @@ -588,18 +615,70 @@ describe('schema transformation with extraction of nested fields', () => { locationName: createMergedResolver({ fromPath: ['location', 'name'] }), //deprecated wrapField shorthand locationName2: wrapField('location', 'name'), + pseudoWrappedError: createMergedResolver({ fromPath: ['error', 'name'] }), }, }, fieldNodeTransformerMap: { 'Property': { - 'locationName': () => toFieldNode('location { name }'), - 'locationName2': () => toFieldNode('location { name }'), + 'locationName': + fieldNode => wrapFieldNode(renameFieldNode(fieldNode, 'name'), ['location']), + 'locationName2': + fieldNode => wrapFieldNode(renameFieldNode(fieldNode, 'name'), ['location']), + 'pseudoWrappedError': fieldNode => renameFieldNode(fieldNode, 'error'), }, }, }), ]); }); + it('should work to extract a field', async () => { + const result = await graphql( + transformedPropertySchema, + ` + query($pid: ID!) { + propertyById(id: $pid) { + test1: locationName + test2: locationName2 + pseudoWrappedError + } + } + `, + {}, + {}, + { + pid: 'p1', + }, + ); + + expect(result).to.deep.equal({ + data: { + propertyById: { + test1: 'Helsinki', + test2: 'Helsinki', + pseudoWrappedError: null, + }, + }, + errors: [ + { + extensions: { + code: 'SOME_CUSTOM_CODE', + }, + locations: [ + { + column: 13, + line: 6, + }, + ], + message: 'Property.error error', + path: [ + 'propertyById', + 'pseudoWrappedError', + ], + }, + ] + }); + }); + it('should work to extract a field', async () => { const result = await graphql( transformedPropertySchema, @@ -759,8 +838,8 @@ describe('schema transformation with renaming of object fields', () => { }, fieldNodeTransformerMap: { 'Property': { - 'new_error': () => toFieldNode('error'), - 'new_error2': () => toFieldNode('error'), + 'new_error': fieldNode => renameFieldNode(fieldNode, 'error'), + 'new_error2': fieldNode => renameFieldNode(fieldNode, 'error'), }, }, }), @@ -799,12 +878,12 @@ describe('schema transformation with renaming of object fields', () => { }, locations: [ { - column: 3, - line: 1, + column: 13, + line: 4, }, { - column: 3, - line: 1, + column: 13, + line: 5, }, ], message: 'Property.error error', @@ -819,12 +898,12 @@ describe('schema transformation with renaming of object fields', () => { }, locations: [ { - column: 3, - line: 1, + column: 13, + line: 4, }, { - column: 3, - line: 1, + column: 13, + line: 5, }, ], message: 'Property.error error',