diff --git a/packages/delegate/src/defaultMergedResolver.ts b/packages/delegate/src/defaultMergedResolver.ts index a023b1d3090..fc4b8dd7959 100644 --- a/packages/delegate/src/defaultMergedResolver.ts +++ b/packages/delegate/src/defaultMergedResolver.ts @@ -17,15 +17,16 @@ export function defaultMergedResolver( context: Record, info: GraphQLResolveInfo ) { + // if parent is nullish, cannot be a proxied result if (!parent) { - return null; + return defaultFieldResolver(parent, args, context, info); } const responseKey = getResponseKeyFromInfo(info); 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 + // all proxied results have an associated array of errors, possibly empty, but always defined + // if errors is nullish, cannot be a proxied result if (!errors) { return defaultFieldResolver(parent, args, context, info); } diff --git a/packages/utils/src/Interfaces.ts b/packages/utils/src/Interfaces.ts index a03fee5e7a6..82dee28c29c 100644 --- a/packages/utils/src/Interfaces.ts +++ b/packages/utils/src/Interfaces.ts @@ -153,7 +153,8 @@ export interface Transform { export type FieldNodeMapper = ( fieldNode: FieldNode, - fragments: Record + fragments: Record, + context: Record ) => SelectionNode | Array; export type FieldNodeMappers = Record>; diff --git a/packages/wrap/src/transforms/MapFields.ts b/packages/wrap/src/transforms/MapFields.ts index 22f91dd642c..b8b0cbdd2cb 100644 --- a/packages/wrap/src/transforms/MapFields.ts +++ b/packages/wrap/src/transforms/MapFields.ts @@ -4,25 +4,49 @@ import { Transform, Request, FieldNodeMappers, ExecutionResult } from '@graphql- import TransformCompositeFields from './TransformCompositeFields'; +import { ObjectValueTransformerMap, ErrorsTransformer } from '../types'; + export default class MapFields implements Transform { private readonly transformer: TransformCompositeFields; - constructor(fieldNodeTransformerMap: FieldNodeMappers) { + constructor( + fieldNodeTransformerMap: FieldNodeMappers, + objectValueTransformerMap?: ObjectValueTransformerMap, + errorsTransformer?: ErrorsTransformer + ) { this.transformer = new TransformCompositeFields( (_typeName, _fieldName, fieldConfig) => fieldConfig, - (typeName, fieldName, fieldNode, fragments) => { + (typeName, fieldName, fieldNode, fragments, context) => { const typeTransformers = fieldNodeTransformerMap[typeName]; if (typeTransformers == null) { - return fieldNode; + return undefined; } const fieldNodeTransformer = typeTransformers[fieldName]; if (fieldNodeTransformer == null) { - return fieldNode; + return undefined; + } + + return fieldNodeTransformer(fieldNode, fragments, context); + }, + data => { + if (data == null) { + return data; + } + + const typeName = data.__typename; + if (typeName == null) { + return data; } - return fieldNodeTransformer(fieldNode, fragments); - } + const transformer = objectValueTransformerMap[typeName]; + if (transformer == null) { + return data; + } + + return transformer(data); + }, + errorsTransformer ); } @@ -30,7 +54,19 @@ export default class MapFields implements Transform { return this.transformer.transformSchema(schema); } - public transformRequest(request: Request): Request { - return this.transformer.transformRequest(request); + public transformRequest( + originalRequest: Request, + delegationContext: Record, + transformationContext: Record + ): Request { + return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + } + + public transformResult( + originalResult: ExecutionResult, + delegationContext: Record, + transformationContext: Record + ): ExecutionResult { + return this.transformer.transformResult(originalResult, delegationContext, transformationContext); } } diff --git a/packages/wrap/src/transforms/TransformCompositeFields.ts b/packages/wrap/src/transforms/TransformCompositeFields.ts index 5b423e07b2a..f2758bbef7e 100644 --- a/packages/wrap/src/transforms/TransformCompositeFields.ts +++ b/packages/wrap/src/transforms/TransformCompositeFields.ts @@ -15,18 +15,28 @@ import { GraphQLObjectType, } from 'graphql'; -import { Transform, Request, MapperKind, mapSchema } from '@graphql-tools/utils'; -import { FieldTransformer, FieldNodeTransformer } from '../types'; +import { Transform, Request, MapperKind, mapSchema, visitData, ExecutionResult } from '@graphql-tools/utils'; +import { FieldTransformer, FieldNodeTransformer, DataTransformer, ErrorsTransformer } from '../types'; export default class TransformCompositeFields implements Transform { private readonly fieldTransformer: FieldTransformer; private readonly fieldNodeTransformer: FieldNodeTransformer; + private readonly dataTransformer: DataTransformer; + private readonly errorsTransformer: ErrorsTransformer; private transformedSchema: GraphQLSchema; + private typeInfo: TypeInfo; private mapping: Record>; - constructor(fieldTransformer: FieldTransformer, fieldNodeTransformer?: FieldNodeTransformer) { + constructor( + fieldTransformer: FieldTransformer, + fieldNodeTransformer?: FieldNodeTransformer, + dataTransformer?: DataTransformer, + errorsTransformer?: ErrorsTransformer + ) { this.fieldTransformer = fieldTransformer; this.fieldNodeTransformer = fieldNodeTransformer; + this.dataTransformer = dataTransformer; + this.errorsTransformer = errorsTransformer; this.mapping = {}; } @@ -35,23 +45,43 @@ export default class TransformCompositeFields implements Transform { [MapperKind.OBJECT_TYPE]: (type: GraphQLObjectType) => this.transformFields(type, this.fieldTransformer), [MapperKind.INTERFACE_TYPE]: (type: GraphQLInterfaceType) => this.transformFields(type, this.fieldTransformer), }); + this.typeInfo = new TypeInfo(this.transformedSchema); return this.transformedSchema; } - public transformRequest(originalRequest: Request): Request { + public transformRequest( + originalRequest: Request, + _delegationContext?: Record, + transformationContext?: Record + ): Request { + const document = originalRequest.document; const fragments = Object.create(null); - originalRequest.document.definitions.forEach(def => { + document.definitions.forEach(def => { if (def.kind === Kind.FRAGMENT_DEFINITION) { fragments[def.name.value] = def; } }); return { ...originalRequest, - document: this.transformDocument(originalRequest.document, fragments), + document: this.transformDocument(document, fragments, transformationContext), }; } + public transformResult( + result: ExecutionResult, + _delegationContext?: Record, + transformationContext?: Record + ) { + if (this.dataTransformer != null) { + result.data = visitData(result.data, value => this.dataTransformer(value, transformationContext)); + } + if (this.errorsTransformer != null) { + result.errors = this.errorsTransformer(result.errors, transformationContext); + } + return result; + } + private transformFields(type: GraphQLObjectType, fieldTransformer: FieldTransformer): GraphQLObjectType; private transformFields(type: GraphQLInterfaceType, fieldTransformer: FieldTransformer): GraphQLInterfaceType; @@ -104,24 +134,25 @@ export default class TransformCompositeFields implements Transform { private transformDocument( document: DocumentNode, - fragments: Record = {} + fragments: Record, + transformationContext: Record ): DocumentNode { - const typeInfo = new TypeInfo(this.transformedSchema); - const newDocument: DocumentNode = visit( + return visit( document, - visitWithTypeInfo(typeInfo, { + visitWithTypeInfo(this.typeInfo, { leave: { - [Kind.SELECTION_SET]: node => this.transformSelectionSet(node, typeInfo, fragments), + [Kind.SELECTION_SET]: node => + this.transformSelectionSet(node, this.typeInfo, fragments, transformationContext), }, }) ); - return newDocument; } private transformSelectionSet( node: SelectionSetNode, typeInfo: TypeInfo, - fragments: Record = {} + fragments: Record, + transformationContext: Record ): SelectionSetNode { const parentType: GraphQLType = typeInfo.getParentType(); if (parentType == null) { @@ -139,10 +170,29 @@ export default class TransformCompositeFields implements Transform { const newName = selection.name.value; - const transformedSelection = - this.fieldNodeTransformer != null - ? this.fieldNodeTransformer(parentTypeName, newName, selection, fragments) - : selection; + if (this.dataTransformer != null || this.errorsTransformer != null) { + newSelections.push({ + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }); + } + + let transformedSelection: SelectionNode | Array; + if (this.fieldNodeTransformer == null) { + transformedSelection = selection; + } else { + transformedSelection = this.fieldNodeTransformer( + parentTypeName, + newName, + selection, + fragments, + transformationContext + ); + transformedSelection = transformedSelection === undefined ? selection : transformedSelection; + } if (Array.isArray(transformedSelection)) { newSelections = newSelections.concat(transformedSelection); diff --git a/packages/wrap/src/transforms/TransformInterfaceFields.ts b/packages/wrap/src/transforms/TransformInterfaceFields.ts index b87734467a4..7c2976e25a3 100644 --- a/packages/wrap/src/transforms/TransformInterfaceFields.ts +++ b/packages/wrap/src/transforms/TransformInterfaceFields.ts @@ -1,6 +1,6 @@ import { GraphQLSchema, isInterfaceType, GraphQLFieldConfig } from 'graphql'; -import { Transform, Request } from '@graphql-tools/utils'; +import { Transform, Request, ExecutionResult } from '@graphql-tools/utils'; import { FieldTransformer, FieldNodeTransformer } from '../types'; import TransformCompositeFields from './TransformCompositeFields'; @@ -33,7 +33,19 @@ export default class TransformInterfaceFields implements Transform { return this.transformer.transformSchema(originalSchema); } - public transformRequest(originalRequest: Request): Request { - return this.transformer.transformRequest(originalRequest); + public transformRequest( + originalRequest: Request, + delegationContext?: Record, + transformationContext?: Record + ): Request { + return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + } + + public transformResult( + originalResult: ExecutionResult, + delegationContext?: Record, + transformationContext?: Record + ): ExecutionResult { + return this.transformer.transformResult(originalResult, delegationContext, transformationContext); } } diff --git a/packages/wrap/src/transforms/TransformObjectFields.ts b/packages/wrap/src/transforms/TransformObjectFields.ts index d4f2a39f8b7..bcf40c0fd90 100644 --- a/packages/wrap/src/transforms/TransformObjectFields.ts +++ b/packages/wrap/src/transforms/TransformObjectFields.ts @@ -1,6 +1,6 @@ import { GraphQLSchema, isObjectType, GraphQLFieldConfig } from 'graphql'; -import { Transform, Request } from '@graphql-tools/utils'; +import { Transform, Request, ExecutionResult } from '@graphql-tools/utils'; import { FieldTransformer, FieldNodeTransformer } from '../types'; import TransformCompositeFields from './TransformCompositeFields'; @@ -33,7 +33,19 @@ export default class TransformObjectFields implements Transform { return this.transformer.transformSchema(originalSchema); } - public transformRequest(originalRequest: Request): Request { - return this.transformer.transformRequest(originalRequest); + public transformRequest( + originalRequest: Request, + delegationContext?: Record, + transformationContext?: Record + ): Request { + return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + } + + public transformResult( + originalResult: ExecutionResult, + delegationContext?: Record, + transformationContext?: Record + ): ExecutionResult { + return this.transformer.transformResult(originalResult, delegationContext, transformationContext); } } diff --git a/packages/wrap/src/transforms/TransformRootFields.ts b/packages/wrap/src/transforms/TransformRootFields.ts index 9d2a78b970e..ed14067e702 100644 --- a/packages/wrap/src/transforms/TransformRootFields.ts +++ b/packages/wrap/src/transforms/TransformRootFields.ts @@ -1,6 +1,6 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; -import { Transform, Request } from '@graphql-tools/utils'; +import { Transform, Request, ExecutionResult } from '@graphql-tools/utils'; import TransformObjectFields from './TransformObjectFields'; import { RootFieldTransformer, FieldNodeTransformer } from '../types'; @@ -45,7 +45,19 @@ export default class TransformRootFields implements Transform { return this.transformer.transformSchema(originalSchema); } - public transformRequest(originalRequest: Request): Request { - return this.transformer.transformRequest(originalRequest); + public transformRequest( + originalRequest: Request, + delegationContext?: Record, + transformationContext?: Record + ): Request { + return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + } + + public transformResult( + originalResult: ExecutionResult, + delegationContext?: Record, + transformationContext?: Record + ): ExecutionResult { + return this.transformer.transformResult(originalResult, delegationContext, transformationContext); } } diff --git a/packages/wrap/src/transforms/WrapFields.ts b/packages/wrap/src/transforms/WrapFields.ts index c6c859c6b70..cc5e7e69ceb 100644 --- a/packages/wrap/src/transforms/WrapFields.ts +++ b/packages/wrap/src/transforms/WrapFields.ts @@ -1,4 +1,4 @@ -import { GraphQLSchema, GraphQLObjectType } from 'graphql'; +import { GraphQLSchema, GraphQLObjectType, GraphQLResolveInfo, GraphQLFieldResolver } from 'graphql'; import { Transform, @@ -7,44 +7,88 @@ import { appendObjectFields, selectObjectFields, modifyObjectFields, + ExecutionResult, + relocatedError, } from '@graphql-tools/utils'; -import { createMergedResolver, defaultMergedResolver } from '@graphql-tools/delegate'; + +import { defaultMergedResolver } from '@graphql-tools/delegate'; import MapFields from './MapFields'; +function defaultWrappingResolver( + parent: any, + args: Record, + context: Record, + info: GraphQLResolveInfo +): any { + if (!parent) { + return {}; + } + + return defaultMergedResolver(parent, args, context, info); +} + export default class WrapFields implements Transform { private readonly outerTypeName: string; private readonly wrappingFieldNames: Array; private readonly wrappingTypeNames: Array; private readonly numWraps: number; private readonly fieldNames: Array; + private readonly wrappingResolver: GraphQLFieldResolver; private readonly transformer: Transform; constructor( outerTypeName: string, wrappingFieldNames: Array, wrappingTypeNames: Array, - fieldNames?: Array + fieldNames?: Array, + wrappingResolver: GraphQLFieldResolver = defaultWrappingResolver ) { this.outerTypeName = outerTypeName; this.wrappingFieldNames = wrappingFieldNames; this.wrappingTypeNames = wrappingTypeNames; this.numWraps = wrappingFieldNames.length; this.fieldNames = fieldNames; + this.wrappingResolver = wrappingResolver; const remainingWrappingFieldNames = this.wrappingFieldNames.slice(); const outerMostWrappingFieldName = remainingWrappingFieldNames.shift(); - this.transformer = new MapFields({ - [outerTypeName]: { - [outerMostWrappingFieldName]: (fieldNode, fragments) => - hoistFieldNodes({ - fieldNode, - path: remainingWrappingFieldNames, - fieldNames: this.fieldNames, - fragments, - }), + this.transformer = new MapFields( + { + [outerTypeName]: { + [outerMostWrappingFieldName]: (fieldNode, fragments) => + hoistFieldNodes({ + fieldNode, + path: remainingWrappingFieldNames, + fieldNames, + fragments, + }), + }, }, - }); + { + [outerTypeName]: value => { + return dehoistResult(value, '__gqltf__'); + }, + }, + errors => { + return errors.map(error => { + const path = error.path; + if (path == null) { + return error; + } + + let newPath: Array = []; + path.forEach(pathSegment => { + if (typeof pathSegment === 'string') { + newPath = newPath.concat(pathSegment.split('__gqltf__')); + } else { + newPath.push(pathSegment); + } + }); + return relocatedError(error, newPath); + }); + } + ); } public transformSchema(schema: GraphQLSchema): GraphQLSchema { @@ -66,7 +110,7 @@ export default class WrapFields implements Transform { newSchema = appendObjectFields(newSchema, nextWrappingTypeName, { [wrappingFieldName]: { type: newSchema.getType(wrappingTypeName) as GraphQLObjectType, - resolve: defaultMergedResolver, + resolve: this.wrappingResolver, }, }); @@ -82,7 +126,7 @@ export default class WrapFields implements Transform { { [wrappingFieldName]: { type: newSchema.getType(wrappingTypeName) as GraphQLObjectType, - resolve: createMergedResolver({ dehoist: true }), + resolve: this.wrappingResolver, }, } ); @@ -90,7 +134,40 @@ export default class WrapFields implements Transform { return this.transformer.transformSchema(newSchema); } - public transformRequest(originalRequest: Request): Request { - return this.transformer.transformRequest(originalRequest); + public transformRequest( + originalRequest: Request, + delegationContext: Record, + transformationContext: Record + ): Request { + return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + } + + public transformResult( + originalResult: ExecutionResult, + delegationContext: Record, + transformationContext: Record + ): ExecutionResult { + return this.transformer.transformResult(originalResult, delegationContext, transformationContext); + } +} + +export function dehoistResult(originalResult: any, delimeter = '__gqltf__'): any { + if (originalResult == null) { + return originalResult; } + + const result = Object.create(null); + + Object.keys(originalResult).forEach(alias => { + let obj = result; + + const fieldNames = alias.split(delimeter); + const fieldName = fieldNames.pop(); + fieldNames.forEach(key => { + obj = obj[key] = obj[key] || Object.create(null); + }); + obj[fieldName] = originalResult[alias]; + }); + + return result; } diff --git a/packages/wrap/src/transforms/WrapType.ts b/packages/wrap/src/transforms/WrapType.ts index 241f12ce88d..9cb57fbde24 100644 --- a/packages/wrap/src/transforms/WrapType.ts +++ b/packages/wrap/src/transforms/WrapType.ts @@ -1,6 +1,6 @@ import { GraphQLSchema } from 'graphql'; -import { Transform, Request } from '@graphql-tools/utils'; +import { Transform, Request, ExecutionResult } from '@graphql-tools/utils'; import WrapFields from './WrapFields'; @@ -8,14 +8,26 @@ export default class WrapType implements Transform { private readonly transformer: Transform; constructor(outerTypeName: string, innerTypeName: string, fieldName: string) { - this.transformer = new WrapFields(outerTypeName, [fieldName], [innerTypeName], undefined); + this.transformer = new WrapFields(outerTypeName, [fieldName], [innerTypeName]); } public transformSchema(schema: GraphQLSchema): GraphQLSchema { return this.transformer.transformSchema(schema); } - public transformRequest(originalRequest: Request): Request { - return this.transformer.transformRequest(originalRequest); + public transformRequest( + originalRequest: Request, + delegationContext: Record, + transformationContext: Record + ): Request { + return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + } + + public transformResult( + originalResult: ExecutionResult, + delegationContext: Record, + transformationContext: Record + ): ExecutionResult { + return this.transformer.transformResult(originalResult, delegationContext, transformationContext); } } diff --git a/packages/wrap/src/types.ts b/packages/wrap/src/types.ts index 521d51b509f..330ce1ba506 100644 --- a/packages/wrap/src/types.ts +++ b/packages/wrap/src/types.ts @@ -9,6 +9,7 @@ import { SelectionNode, ObjectFieldNode, ObjectValueNode, + GraphQLError, } from 'graphql'; import { Executor, Subscriber, DelegationContext } from '@graphql-tools/delegate'; import { Request } from '@graphql-tools/utils'; @@ -58,5 +59,15 @@ export type FieldNodeTransformer = ( typeName: string, fieldName: string, fieldNode: FieldNode, - fragments: Record + fragments: Record, + context: Record ) => SelectionNode | Array; + +export type DataTransformer = (value: any, context?: Record) => any; + +export type ObjectValueTransformerMap = Record; + +export type ErrorsTransformer = ( + errors: ReadonlyArray, + context: Record +) => Array;