From c1810bbd36e001c025eedf0912bfb322d71e1291 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 29 Jun 2020 23:34:55 -0400 Subject: [PATCH 1/4] refactor TransformCompositeFields --- .../transforms/TransformCompositeFields.ts | 148 +++++++++--------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/packages/wrap/src/transforms/TransformCompositeFields.ts b/packages/wrap/src/transforms/TransformCompositeFields.ts index c9b9c292547..5b423e07b2a 100644 --- a/packages/wrap/src/transforms/TransformCompositeFields.ts +++ b/packages/wrap/src/transforms/TransformCompositeFields.ts @@ -41,20 +41,14 @@ export default class TransformCompositeFields implements Transform { public transformRequest(originalRequest: Request): Request { const fragments = Object.create(null); - originalRequest.document.definitions - .filter(def => def.kind === Kind.FRAGMENT_DEFINITION) - .forEach(def => { - fragments[(def as FragmentDefinitionNode).name.value] = def; - }); - const document = this.transformDocument( - originalRequest.document, - this.mapping, - this.fieldNodeTransformer, - fragments - ); + originalRequest.document.definitions.forEach(def => { + if (def.kind === Kind.FRAGMENT_DEFINITION) { + fragments[def.name.value] = def; + } + }); return { ...originalRequest, - document, + document: this.transformDocument(originalRequest.document, fragments), }; } @@ -110,8 +104,6 @@ export default class TransformCompositeFields implements Transform { private transformDocument( document: DocumentNode, - mapping: Record>, - fieldNodeTransformer?: FieldNodeTransformer, fragments: Record = {} ): DocumentNode { const typeInfo = new TypeInfo(this.transformedSchema); @@ -119,69 +111,77 @@ export default class TransformCompositeFields implements Transform { document, visitWithTypeInfo(typeInfo, { leave: { - [Kind.SELECTION_SET]: (node: SelectionSetNode): SelectionSetNode => { - const parentType: GraphQLType = typeInfo.getParentType(); - if (parentType != null) { - const parentTypeName = parentType.name; - let newSelections: Array = []; - - node.selections.forEach(selection => { - if (selection.kind !== Kind.FIELD) { - newSelections.push(selection); - return; - } - - const newName = selection.name.value; - - const transformedSelection = - fieldNodeTransformer != null - ? fieldNodeTransformer(parentTypeName, newName, selection, fragments) - : selection; - - if (Array.isArray(transformedSelection)) { - newSelections = newSelections.concat(transformedSelection); - return; - } - - if (transformedSelection.kind !== Kind.FIELD) { - newSelections.push(transformedSelection); - return; - } - - const typeMapping = mapping[parentTypeName]; - if (typeMapping == null) { - newSelections.push(transformedSelection); - return; - } - - const oldName = mapping[parentTypeName][newName]; - if (oldName == null) { - newSelections.push(transformedSelection); - return; - } - - newSelections.push({ - ...transformedSelection, - name: { - kind: Kind.NAME, - value: oldName, - }, - alias: { - kind: Kind.NAME, - value: newName, - }, - }); - }); - - return { - ...node, - selections: newSelections, - }; - } - }, + [Kind.SELECTION_SET]: node => this.transformSelectionSet(node, typeInfo, fragments), }, }) ); return newDocument; } + + private transformSelectionSet( + node: SelectionSetNode, + typeInfo: TypeInfo, + fragments: Record = {} + ): SelectionSetNode { + const parentType: GraphQLType = typeInfo.getParentType(); + if (parentType == null) { + return undefined; + } + + const parentTypeName = parentType.name; + let newSelections: Array = []; + + node.selections.forEach(selection => { + if (selection.kind !== Kind.FIELD) { + newSelections.push(selection); + return; + } + + const newName = selection.name.value; + + const transformedSelection = + this.fieldNodeTransformer != null + ? this.fieldNodeTransformer(parentTypeName, newName, selection, fragments) + : selection; + + if (Array.isArray(transformedSelection)) { + newSelections = newSelections.concat(transformedSelection); + return; + } + + if (transformedSelection.kind !== Kind.FIELD) { + newSelections.push(transformedSelection); + return; + } + + const typeMapping = this.mapping[parentTypeName]; + if (typeMapping == null) { + newSelections.push(transformedSelection); + return; + } + + const oldName = this.mapping[parentTypeName][newName]; + if (oldName == null) { + newSelections.push(transformedSelection); + return; + } + + newSelections.push({ + ...transformedSelection, + name: { + kind: Kind.NAME, + value: oldName, + }, + alias: { + kind: Kind.NAME, + value: newName, + }, + }); + }); + + return { + ...node, + selections: newSelections, + }; + } } From 7a318513188e716db18ccf280a6701dc50a3591e Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 29 Jun 2020 14:52:11 -0400 Subject: [PATCH 2/4] introduce result visiting This changes introduces a few generic functions for revisiting data = visitData can be used to recursively visit an ExecutionResult (or any object) it takes two functions, one executed when entering the object, one when leaving = visitResult can be used to visit a result by with a resultVisitorMap and/or an errorVisitorMap visitResult visits the result using the request, so it knows the object type for every object within the map, as long as the result includes __typename info when requesting abstract types, and also knows the field name for each aliased key within the object. * it executes the correct visitor from each objectVisitorMap included within the resultVisitorMap depending on the object type and field name * it executes object visitors on the object itself when entering and leaving the object using the values from the __enter and __leave dummy field names within the objectVisitorMap * it executes any visitors for leaf types included within the resultVisitorMap to provide a simple mechanism of visiting all fields of a certain leaf type errors can also be visited -- these are meant to provide opportunities for transforming a GraphQLError, including the path, and so, if an errorVisitorMap is included, error visitors from the map will be collected for each field found in the path of the original error. --- packages/utils/src/index.ts | 1 + packages/utils/src/visitResult.ts | 393 ++++++++++++++++++++ packages/utils/tests/visitResult.test.ts | 385 +++++++++++++++++++ packages/wrap/src/transforms/RenameTypes.ts | 45 +-- 4 files changed, 795 insertions(+), 29 deletions(-) create mode 100644 packages/utils/src/visitResult.ts create mode 100644 packages/utils/tests/visitResult.test.ts diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index aaaa5ab27f6..d736ed116a0 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -46,3 +46,4 @@ export * from './implementsAbstractType'; export * from './errors'; export * from './toConfig'; export * from './observableToAsyncIterable'; +export * from './visitResult'; diff --git a/packages/utils/src/visitResult.ts b/packages/utils/src/visitResult.ts new file mode 100644 index 00000000000..7ee0135ab0b --- /dev/null +++ b/packages/utils/src/visitResult.ts @@ -0,0 +1,393 @@ +import { + GraphQLSchema, + getOperationRootType, + getOperationAST, + Kind, + GraphQLObjectType, + FieldNode, + GraphQLOutputType, + isListType, + getNullableType, + isAbstractType, + isObjectType, + OperationDefinitionNode, + GraphQLError, +} from 'graphql'; + +import { Request, GraphQLExecutionContext, ExecutionResult } from './Interfaces'; +import { collectFields } from './collectFields'; + +export type ValueVisitor = (value: any) => any; + +export type ObjectValueVisitor = { + __enter?: ValueVisitor; + __leave?: ValueVisitor; +} & Record; + +export type ResultVisitorMap = Record; + +export type ErrorVisitor = (error: GraphQLError, pathIndex: number) => GraphQLError; + +export type ErrorVisitorMap = Record>; + +interface SegmentInfo { + type: GraphQLObjectType; + fieldName: string; + pathIndex: number; +} + +interface ErrorInfo { + segmentInfoMap: Map>; + unpathedErrors: Array; +} + +interface SortedErrors { + errorMap: Record>; + unpathedErrors: Array; +} + +export function visitData(data: any, enter?: ValueVisitor, leave?: ValueVisitor): any { + if (Array.isArray(data)) { + return data.map(value => visitData(value, enter, leave)); + } else if (typeof data === 'object') { + const newData = enter != null ? enter(data) : data; + + if (newData != null) { + Object.keys(newData).forEach(key => { + const value = newData[key]; + newData[key] = visitData(value, enter, leave); + }); + } + + return leave != null ? leave(newData) : newData; + } + + return data; +} + +export function visitErrors( + errors: ReadonlyArray, + visitor: (error: GraphQLError) => GraphQLError +): Array { + return errors.map(error => visitor(error)); +} +export function visitResult( + result: ExecutionResult, + request: Request, + schema: GraphQLSchema, + resultVisitorMap?: ResultVisitorMap, + errorVisitorMap?: ErrorVisitorMap +): any { + const partialExecutionContext = { + schema, + fragments: request.document.definitions.reduce((acc, def) => { + if (def.kind === Kind.FRAGMENT_DEFINITION) { + acc[def.name.value] = def; + } + return acc; + }, {}), + variableValues: request.variables, + } as GraphQLExecutionContext; + + const errorInfo: ErrorInfo = { + segmentInfoMap: new Map>(), + unpathedErrors: [], + }; + + const data = result.data; + const errors = result.errors; + const visitingErrors = errors != null && errorVisitorMap != null; + + if (data != null) { + result.data = visitRoot( + data, + getOperationAST(request.document, undefined), + partialExecutionContext, + resultVisitorMap, + visitingErrors ? errors : undefined, + errorInfo + ); + } + + if (visitingErrors) { + result.errors = visitErrorsByType(errors, errorVisitorMap, errorInfo); + } + + return result; +} + +function visitErrorsByType( + errors: ReadonlyArray, + errorVisitorMap: ErrorVisitorMap, + errorInfo: ErrorInfo +): Array { + return errors.map(error => { + const pathSegmentsInfo = errorInfo.segmentInfoMap.get(error); + if (pathSegmentsInfo == null) { + return error; + } + return pathSegmentsInfo.reduceRight((acc, segmentInfo) => { + const typeName = segmentInfo.type.name; + const typeVisitorMap = errorVisitorMap[typeName]; + if (typeVisitorMap == null) { + return acc; + } + const errorVisitor = typeVisitorMap[segmentInfo.fieldName]; + return errorVisitor == null ? acc : errorVisitor(acc as GraphQLError, segmentInfo.pathIndex); + }, error) as GraphQLError; + }); +} + +function visitRoot( + root: any, + operation: OperationDefinitionNode, + exeContext: GraphQLExecutionContext, + resultVisitorMap: ResultVisitorMap, + errors: ReadonlyArray, + errorInfo: ErrorInfo +): any { + const operationRootType = getOperationRootType(exeContext.schema, operation); + const collectedFields = collectFields( + exeContext, + operationRootType, + operation.selectionSet, + Object.create(null), + Object.create(null) + ); + + return visitObjectValue(root, operationRootType, collectedFields, exeContext, resultVisitorMap, 0, errors, errorInfo); +} + +function visitObjectValue( + object: Record, + type: GraphQLObjectType, + fieldNodeMap: Record>, + exeContext: GraphQLExecutionContext, + resultVisitorMap: ResultVisitorMap, + pathIndex: number, + errors: ReadonlyArray, + errorInfo: ErrorInfo +): Record { + const fieldMap = type.getFields(); + const typeVisitorMap = resultVisitorMap?.[type.name] as ObjectValueVisitor; + + const enterObject = typeVisitorMap?.__enter as ValueVisitor; + const newObject = enterObject != null ? enterObject(object) : object; + + let sortedErrors: SortedErrors; + let errorMap: Record>; + if (errors != null) { + sortedErrors = sortErrorsByPathSegment(errors, pathIndex); + errorMap = sortedErrors.errorMap; + errorInfo.unpathedErrors = errorInfo.unpathedErrors.concat(sortedErrors.unpathedErrors); + } + + Object.keys(fieldNodeMap).forEach(responseKey => { + const subFieldNodes = fieldNodeMap[responseKey]; + const fieldName = subFieldNodes[0].name.value; + const fieldType = fieldMap[fieldName].type; + + const newPathIndex = pathIndex + 1; + + let fieldErrors: Array; + if (errors != null) { + fieldErrors = errorMap[responseKey]; + if (fieldErrors != null) { + delete errorMap[responseKey]; + } + addPathSegmentInfo(type, fieldName, newPathIndex, fieldErrors, errorInfo); + } + + const newValue = visitFieldValue( + object[responseKey], + fieldType, + subFieldNodes, + exeContext, + resultVisitorMap, + newPathIndex, + fieldErrors, + errorInfo + ); + + updateObject(newObject, responseKey, newValue, typeVisitorMap, fieldName); + }); + + const oldTypename = newObject.__typename; + if (oldTypename != null) { + updateObject(newObject, '__typename', oldTypename, typeVisitorMap, '__typename'); + } + + if (errors != null) { + Object.keys(errorMap).forEach(unknownResponseKey => { + errorInfo.unpathedErrors = errorInfo.unpathedErrors.concat(errorMap[unknownResponseKey]); + }); + } + + const leaveObject = typeVisitorMap?.__leave as ValueVisitor; + + return leaveObject != null ? leaveObject(newObject) : newObject; +} + +function updateObject( + object: Record, + responseKey: string, + newValue: any, + typeVisitorMap: ObjectValueVisitor, + fieldName: string +): void { + if (typeVisitorMap == null) { + object[responseKey] = newValue; + return; + } + + const fieldVisitor = typeVisitorMap[fieldName]; + if (fieldVisitor == null) { + object[responseKey] = newValue; + return; + } + + const visitedValue = fieldVisitor(newValue); + if (visitedValue === undefined) { + delete object[responseKey]; + return; + } + + object[responseKey] = visitedValue; +} + +function visitListValue( + list: Array, + returnType: GraphQLOutputType, + fieldNodes: Array, + exeContext: GraphQLExecutionContext, + resultVisitorMap: ResultVisitorMap, + pathIndex: number, + errors: ReadonlyArray, + errorInfo: ErrorInfo +): Array { + return list.map(listMember => + visitFieldValue(listMember, returnType, fieldNodes, exeContext, resultVisitorMap, pathIndex + 1, errors, errorInfo) + ); +} + +function visitFieldValue( + value: any, + returnType: GraphQLOutputType, + fieldNodes: Array, + exeContext: GraphQLExecutionContext, + resultVisitorMap: ResultVisitorMap, + pathIndex: number, + errors: ReadonlyArray = [], + errorInfo: ErrorInfo +): any { + if (value == null) { + return value; + } + + const nullableType = getNullableType(returnType); + if (isListType(nullableType)) { + return visitListValue( + value as Array, + nullableType.ofType, + fieldNodes, + exeContext, + resultVisitorMap, + pathIndex, + errors, + errorInfo + ); + } else if (isAbstractType(nullableType)) { + const finalType = exeContext.schema.getType(value.__typename) as GraphQLObjectType; + const collectedFields = collectSubFields(exeContext, finalType, fieldNodes); + return visitObjectValue( + value, + finalType, + collectedFields, + exeContext, + resultVisitorMap, + pathIndex, + errors, + errorInfo + ); + } else if (isObjectType(nullableType)) { + const collectedFields = collectSubFields(exeContext, nullableType, fieldNodes); + return visitObjectValue( + value, + nullableType, + collectedFields, + exeContext, + resultVisitorMap, + pathIndex, + errors, + errorInfo + ); + } + + const typeVisitorMap = resultVisitorMap?.[nullableType.name] as ValueVisitor; + if (typeVisitorMap == null) { + return value; + } + + const visitedValue = typeVisitorMap(value); + return visitedValue === undefined ? value : visitedValue; +} + +function sortErrorsByPathSegment(errors: ReadonlyArray, pathIndex: number): SortedErrors { + const errorMap = Object.create(null); + const unpathedErrors: Array = []; + errors.forEach(error => { + const pathSegment = error.path?.[pathIndex]; + if (pathSegment == null) { + unpathedErrors.push(error); + return; + } + + if (pathSegment in errorMap) { + errorMap[pathSegment].push(error); + } else { + errorMap[pathSegment] = [error]; + } + }); + + return { + errorMap, + unpathedErrors, + }; +} + +function addPathSegmentInfo( + type: GraphQLObjectType, + fieldName: string, + pathIndex: number, + errors: ReadonlyArray = [], + errorInfo: ErrorInfo +) { + errors.forEach(error => { + const segmentInfo = { + type, + fieldName, + pathIndex, + }; + const pathSegmentsInfo = errorInfo.segmentInfoMap.get(error); + if (pathSegmentsInfo == null) { + errorInfo.segmentInfoMap.set(error, [segmentInfo]); + } else { + pathSegmentsInfo.push(segmentInfo); + } + }); +} + +function collectSubFields( + exeContext: GraphQLExecutionContext, + type: GraphQLObjectType, + fieldNodes: Array +): Record> { + let subFieldNodes: Record> = Object.create(null); + const visitedFragmentNames = Object.create(null); + + fieldNodes.forEach(fieldNode => { + subFieldNodes = collectFields(exeContext, type, fieldNode.selectionSet, subFieldNodes, visitedFragmentNames); + }); + + return subFieldNodes; +} diff --git a/packages/utils/tests/visitResult.test.ts b/packages/utils/tests/visitResult.test.ts new file mode 100644 index 00000000000..0cdd18109a1 --- /dev/null +++ b/packages/utils/tests/visitResult.test.ts @@ -0,0 +1,385 @@ +import { buildSchema, parse, GraphQLError } from 'graphql'; + +import { ExecutionResult } from '@graphql-tools/utils'; + +import { relocatedError } from '../src/errors'; + +import { visitResult } from '../src/visitResult'; + +describe('visiting results', () => { + const schema = buildSchema(` + interface TestInterface { + field: String + } + type Test { + field: String + } + type Query { + test: TestInterface + } + `); + + const request = { + document: parse('{ test { field } }'), + variables: {}, + }; + + it('should visit without throwing', async () => { + expect(() => visitResult({}, request, schema, undefined)).not.toThrow(); + }); + + it('should allow visiting without a resultVisitorMap', async () => { + const result = { + data: { + test: { + __typename: 'Test', + field: 'test', + }, + }, + }; + + const visitedResult = visitResult(result, request, schema, undefined); + expect(visitedResult).toEqual(result); + }); + + it('should successfully modify the result using an object type result visitor', async () => { + const result = { + data: { + test: { + __typename: 'Test', + field: 'test', + }, + }, + }; + + const visitedResult = visitResult(result, request, schema, { + Test: { + field: () => 'success', + }, + }); + + const expectedResult = { + data: { + test: { + __typename: 'Test', + field: 'success', + }, + }, + }; + + expect(visitedResult).toEqual(expectedResult); + }); + + it('should successfully modify the result using a leaf type result visitor', async () => { + const result = { + data: { + test: { + __typename: 'Test', + field: 'test', + }, + }, + }; + + const visitedResult = visitResult(result, request, schema, { + String: () => 'success', + }); + + const expectedResult = { + data: { + test: { + __typename: 'Test', + field: 'success', + }, + }, + }; + + expect(visitedResult).toEqual(expectedResult); + }); + + it('should successfully modify the result using both leaf type and object type visitors', async () => { + const result = { + data: { + test: { + __typename: 'Test', + field: 'test', + }, + }, + }; + + const visitedResult = visitResult(result, request, schema, { + Test: { + // leaf type visitors fire first. + field: (value) => value === 'intermediate' ? 'success' : 'failure', + }, + String: () => 'intermediate', + }); + + const expectedResult = { + data: { + test: { + __typename: 'Test', + field: 'success', + }, + }, + }; + + expect(visitedResult).toEqual(expectedResult); + }); + + it('should successfully modify the __typename field of an object', async () => { + const result = { + data: { + test: { + __typename: 'Test', + field: 'test', + }, + }, + }; + + const visitedResult = visitResult(result, request, schema, { + Test: { + __typename: () => 'Success', + }, + }); + + const expectedResult = { + data: { + test: { + __typename: 'Success', + field: 'test', + }, + }, + }; + + expect(visitedResult).toEqual(expectedResult); + }); + + it('should successfully modify the object directly using the __leave field of an object type result visitor', async () => { + const result = { + data: { + test: { + __typename: 'Test', + field: 'test', + }, + }, + }; + + const visitedResult = visitResult(result, request, schema, { + Test: { + __leave: (object) => ({ + ...object, + __typename: 'Success', + }), + }, + }); + + const expectedResult = { + data: { + test: { + __typename: 'Success', + field: 'test', + }, + }, + }; + + expect(visitedResult).toEqual(expectedResult); + }); +}); + +describe('visiting nested results', () => { + const schema = buildSchema(` + type User { + name: String + } + type UserGroup { + name: String + subGroupedUsers: [[User]] + } + type Query { + userGroups: [UserGroup] + } + `); + + const request = { + document: parse(`{ + userGroups { + name + subGroupedUsers { + name + } + } + }`), + variables: {}, + }; + + it('should work', async () => { + const result: ExecutionResult = { + data: { + userGroups: [{ + name: 'Group A', + subGroupedUsers: [[ + { + name: 'User A', + } + ]] + }], + }, + }; + + const visitedResult = visitResult(result, request, schema, {}); + expect(visitedResult).toEqual(result); + }); + + it('should work when the parent is null', async () => { + const result: ExecutionResult = { + data: { + userGroups: null, + }, + }; + + const visitedResult = visitResult(result, request, schema, {}); + expect(visitedResult).toEqual(result); + }); + + it('should work when the parent is an empty list', async () => { + const result: ExecutionResult = { + data: { + userGroups: [], + }, + }; + + const visitedResult = visitResult(result, request, schema, {}); + expect(visitedResult).toEqual(result); + }); +}); + +describe('visiting nested results', () => { + const schema = buildSchema(` + type User { + name: String + } + type UserGroup { + name: String + subGroupedUsers: [[User]] + } + type Query { + userGroups: [UserGroup] + } + `); + + const request = { + document: parse(`{ + userGroups { + name + subGroupedUsers { + name + } + } + }`), + variables: {}, + }; + + it('should work', async () => { + const result: ExecutionResult = { + data: { + userGroups: [{ + name: 'Group A', + subGroupedUsers: [[ + { + name: 'User A', + } + ]] + }], + }, + }; + + const visitedResult = visitResult(result, request, schema, {}); + expect(visitedResult).toEqual(result); + }); + + it('should work when the parent is null', async () => { + const result: ExecutionResult = { + data: { + userGroups: null, + }, + }; + + const visitedResult = visitResult(result, request, schema, {}); + expect(visitedResult).toEqual(result); + }); + + it('should work when the parent is an empty list', async () => { + const result: ExecutionResult = { + data: { + userGroups: [], + }, + }; + + const visitedResult = visitResult(result, request, schema, {}); + expect(visitedResult).toEqual(result); + }); +}); + +describe('visiting errors', () => { + const schema = buildSchema(` + interface TestInterface { + field: String + } + type Test { + field: String + } + type Query { + test: TestInterface + } + `); + + const request = { + document: parse('{ test { field } }'), + variables: {}, + }; + + it('should allow visiting without an errorVisitor', async () => { + const result: ExecutionResult = { + data: { + test: { + __typename: 'Test', + field: null, + }, + }, + errors: [ + new GraphQLError('unpathed error'), + new GraphQLError('pathed error', undefined, undefined, undefined, ['test', 'field']), + ] + }; + + const visitedResult = visitResult(result, request, schema, undefined, undefined); + expect(visitedResult).toEqual(result); + }); + + it('should allow visiting with an errorVisitorMap', async () => { + const result: ExecutionResult = { + data: { + test: { + __typename: 'Test', + field: null, + }, + }, + errors: [ + new GraphQLError('unpathed error'), + new GraphQLError('pathed error', undefined, undefined, undefined, ['test', 'field']), + ] + }; + + const visitedResult = visitResult(result, request, schema, undefined, { + Query: { + test: (error, pathIndex) => { + const oldPath = error.path; + const newPath = [...oldPath.slice(0, pathIndex), 'inserted', ...oldPath.slice(pathIndex)]; + return relocatedError(error, newPath); + }, + }, + }); + + expect(visitedResult.errors[1].path).toEqual(['test', 'inserted', 'field']); + }); +}); diff --git a/packages/wrap/src/transforms/RenameTypes.ts b/packages/wrap/src/transforms/RenameTypes.ts index 94ddb5adf63..4a6c7a0bb23 100644 --- a/packages/wrap/src/transforms/RenameTypes.ts +++ b/packages/wrap/src/transforms/RenameTypes.ts @@ -19,7 +19,15 @@ import { visit, } from 'graphql'; -import { Transform, Request, ExecutionResult, MapperKind, RenameTypesOptions, mapSchema } from '@graphql-tools/utils'; +import { + Transform, + Request, + ExecutionResult, + MapperKind, + RenameTypesOptions, + mapSchema, + visitData, +} from '@graphql-tools/utils'; export default class RenameTypes implements Transform { private readonly renamer: (name: string) => string | undefined; @@ -119,34 +127,13 @@ export default class RenameTypes implements Transform { public transformResult(result: ExecutionResult): ExecutionResult { return { ...result, - data: this.transformData(result.data), - }; - } - - private transformData(data: any): any { - if (data == null) { - return data; - } else if (Array.isArray(data)) { - return data.map(value => this.transformData(value)); - } else if (typeof data === 'object') { - return this.transformObject(data); - } - - return data; - } - - private transformObject(object: Record): Record { - Object.keys(object).forEach(key => { - const value = object[key]; - if (key === '__typename') { - if (value in this.map) { - object[key] = this.map[value]; + data: visitData(result.data, object => { + const typeName = object?.__typename; + if (typeName != null && typeName in this.map) { + object.__typename = this.map[typeName]; } - } else { - object[key] = this.transformData(value); - } - }); - - return object; + return object; + }), + }; } } From dcc75ac05b3aa384cb531a686055f6b52286b7d3 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 3 Jul 2020 16:06:33 -0400 Subject: [PATCH 3/4] Add result transforming to bundled transformers Adds result wrapping capability to the following generic transformers = TransformCompositeFields = TransformInterfaceFields = TransformObjectFields = TransformRootFields = ExtendSchema = MapFields Adds result visiting usage to the following transfromers = WrapFields = WrapType = HoistField Result visiting was introduced within RenameTypes within the initial commit. --- packages/utils/src/Interfaces.ts | 3 +- packages/wrap/src/transforms/ExtendSchema.ts | 37 ++- packages/wrap/src/transforms/HoistField.ts | 106 +++++++- packages/wrap/src/transforms/MapFields.ts | 56 +++- .../transforms/TransformCompositeFields.ts | 84 ++++-- .../transforms/TransformInterfaceFields.ts | 18 +- .../src/transforms/TransformObjectFields.ts | 18 +- .../src/transforms/TransformRootFields.ts | 18 +- packages/wrap/src/transforms/WrapFields.ts | 257 ++++++++++++++++-- packages/wrap/src/transforms/WrapType.ts | 20 +- packages/wrap/src/types.ts | 13 +- 11 files changed, 552 insertions(+), 78 deletions(-) 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/ExtendSchema.ts b/packages/wrap/src/transforms/ExtendSchema.ts index 12834be0d10..41e243a001d 100644 --- a/packages/wrap/src/transforms/ExtendSchema.ts +++ b/packages/wrap/src/transforms/ExtendSchema.ts @@ -1,9 +1,18 @@ import { GraphQLSchema, extendSchema, parse } from 'graphql'; -import { Transform, IFieldResolver, IResolvers, Request, FieldNodeMappers } from '@graphql-tools/utils'; +import { + Transform, + IFieldResolver, + IResolvers, + Request, + FieldNodeMappers, + ExecutionResult, +} from '@graphql-tools/utils'; import { addResolversToSchema } from '@graphql-tools/schema'; import { defaultMergedResolver } from '@graphql-tools/delegate'; +import { ObjectValueTransformerMap, ErrorsTransformer } from '../types'; + import MapFields from './MapFields'; export default class ExtendSchema implements Transform { @@ -17,16 +26,24 @@ export default class ExtendSchema implements Transform { resolvers = {}, defaultFieldResolver, fieldNodeTransformerMap, + objectValueTransformerMap, + errorsTransformer, }: { typeDefs?: string; resolvers?: IResolvers; defaultFieldResolver?: IFieldResolver; fieldNodeTransformerMap?: FieldNodeMappers; + objectValueTransformerMap?: ObjectValueTransformerMap; + errorsTransformer?: ErrorsTransformer; }) { this.typeDefs = typeDefs; this.resolvers = resolvers; this.defaultFieldResolver = defaultFieldResolver != null ? defaultFieldResolver : defaultMergedResolver; - this.transformer = new MapFields(fieldNodeTransformerMap != null ? fieldNodeTransformerMap : {}); + this.transformer = new MapFields( + fieldNodeTransformerMap != null ? fieldNodeTransformerMap : {}, + objectValueTransformerMap, + errorsTransformer + ); } public transformSchema(schema: GraphQLSchema): GraphQLSchema { @@ -41,7 +58,19 @@ export default class ExtendSchema implements Transform { }); } - 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/HoistField.ts b/packages/wrap/src/transforms/HoistField.ts index 3973925ea1c..3d0a7206ff1 100644 --- a/packages/wrap/src/transforms/HoistField.ts +++ b/packages/wrap/src/transforms/HoistField.ts @@ -1,37 +1,46 @@ -import { GraphQLSchema, GraphQLObjectType, getNullableType } from 'graphql'; +import { GraphQLSchema, GraphQLObjectType, getNullableType, FieldNode, Kind, GraphQLError } from 'graphql'; import { - wrapFieldNode, renameFieldNode, appendObjectFields, removeObjectFields, Transform, Request, + ExecutionResult, + relocatedError, } from '@graphql-tools/utils'; +import { defaultMergedResolver } from '@graphql-tools/delegate'; + import MapFields from './MapFields'; -import { createMergedResolver } from '@graphql-tools/delegate'; export default class HoistField implements Transform { private readonly typeName: string; - private readonly path: Array; private readonly newFieldName: string; private readonly pathToField: Array; private readonly oldFieldName: string; private readonly transformer: Transform; - constructor(typeName: string, path: Array, newFieldName: string) { + constructor(typeName: string, path: Array, newFieldName: string, alias = '__gqtlw__') { this.typeName = typeName; - this.path = path; this.newFieldName = newFieldName; - this.pathToField = this.path.slice(); - this.oldFieldName = this.pathToField.pop(); - this.transformer = new MapFields({ - [typeName]: { - [newFieldName]: fieldNode => wrapFieldNode(renameFieldNode(fieldNode, this.oldFieldName), this.pathToField), + const pathToField = path.slice(); + const oldFieldName = pathToField.pop(); + + this.oldFieldName = oldFieldName; + this.pathToField = pathToField; + this.transformer = new MapFields( + { + [typeName]: { + [newFieldName]: fieldNode => wrapFieldNode(renameFieldNode(fieldNode, oldFieldName), pathToField, alias), + }, }, - }); + { + [typeName]: value => unwrapValue(value, alias), + }, + errors => unwrapErrors(errors, alias) + ); } public transformSchema(schema: GraphQLSchema): GraphQLSchema { @@ -53,14 +62,81 @@ export default class HoistField implements Transform { newSchema = appendObjectFields(newSchema, this.typeName, { [this.newFieldName]: { type: targetType, - resolve: createMergedResolver({ fromPath: this.pathToField }), + resolve: defaultMergedResolver, }, }); 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 wrapFieldNode(fieldNode: FieldNode, path: Array, alias: string): FieldNode { + let newFieldNode = fieldNode; + path.forEach(fieldName => { + newFieldNode = { + kind: Kind.FIELD, + alias: { + kind: Kind.NAME, + value: alias, + }, + name: { + kind: Kind.NAME, + value: fieldName, + }, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [fieldNode], + }, + }; + }); + + return newFieldNode; +} + +export function unwrapValue(originalValue: any, alias: string): any { + let newValue = originalValue; + + let object = newValue[alias]; + while (object != null) { + newValue = object; + object = newValue[alias]; + } + + delete originalValue[alias]; + Object.assign(originalValue, newValue); + + return originalValue; +} + +function unwrapErrors(errors: ReadonlyArray, alias: string): Array { + if (errors === undefined) { + return undefined; + } + + return errors.map(error => { + const originalPath = error.path; + if (originalPath == null) { + return error; + } + + const newPath = originalPath.filter(pathSegment => pathSegment !== alias); + + return relocatedError(error, newPath); + }); +} diff --git a/packages/wrap/src/transforms/MapFields.ts b/packages/wrap/src/transforms/MapFields.ts index 79c604bb207..190edb560ef 100644 --- a/packages/wrap/src/transforms/MapFields.ts +++ b/packages/wrap/src/transforms/MapFields.ts @@ -1,28 +1,54 @@ import { GraphQLSchema } from 'graphql'; -import { Transform, Request, FieldNodeMappers } from '@graphql-tools/utils'; +import { Transform, Request, FieldNodeMappers, ExecutionResult } from '@graphql-tools/utils'; 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); - } + return fieldNodeTransformer(fieldNode, fragments, context); + }, + objectValueTransformerMap != null + ? (data, context) => { + if (data == null) { + return data; + } + + const typeName = data.__typename; + if (typeName == null) { + return data; + } + + const transformer = objectValueTransformerMap[typeName]; + if (transformer == null) { + return data; + } + + return transformer(data, context); + } + : undefined, + errorsTransformer != null ? errorsTransformer : undefined ); } @@ -30,7 +56,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..bc4fad8bcb4 100644 --- a/packages/wrap/src/transforms/WrapFields.ts +++ b/packages/wrap/src/transforms/WrapFields.ts @@ -1,50 +1,92 @@ -import { GraphQLSchema, GraphQLObjectType } from 'graphql'; +import { + GraphQLSchema, + GraphQLObjectType, + GraphQLResolveInfo, + GraphQLFieldResolver, + GraphQLError, + FieldNode, + FragmentDefinitionNode, + SelectionSetNode, + Kind, +} from 'graphql'; import { Transform, Request, - hoistFieldNodes, 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'; +interface WrapFieldsTransformationContext { + nextIndex: number; + paths: Record; alias: string }>; +} + +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, + prefix = 'gqtld' ) { 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, context: WrapFieldsTransformationContext) => + hoistFieldNodes({ + fieldNode, + path: remainingWrappingFieldNames, + fieldNames, + fragments, + context, + prefix, + }), + }, }, - }); + { + [outerTypeName]: (value, context: WrapFieldsTransformationContext) => dehoistValue(value, context), + }, + (errors, context: WrapFieldsTransformationContext) => dehoistErrors(errors, context) + ); } public transformSchema(schema: GraphQLSchema): GraphQLSchema { @@ -66,7 +108,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 +124,7 @@ export default class WrapFields implements Transform { { [wrappingFieldName]: { type: newSchema.getType(wrappingTypeName) as GraphQLObjectType, - resolve: createMergedResolver({ dehoist: true }), + resolve: this.wrappingResolver, }, } ); @@ -90,7 +132,186 @@ 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: WrapFieldsTransformationContext + ): Request { + transformationContext.nextIndex = 0; + transformationContext.paths = Object.create(null); + return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); } + + public transformResult( + originalResult: ExecutionResult, + delegationContext: Record, + transformationContext: WrapFieldsTransformationContext + ): ExecutionResult { + return this.transformer.transformResult(originalResult, delegationContext, transformationContext); + } +} + +function collectFields( + selectionSet: SelectionSetNode, + fragments: Record, + fields: Array = [], + visitedFragmentNames = {} +): Array { + if (selectionSet != null) { + selectionSet.selections.forEach(selection => { + switch (selection.kind) { + case Kind.FIELD: + fields.push(selection); + break; + case Kind.INLINE_FRAGMENT: + collectFields(selection.selectionSet, fragments, fields, visitedFragmentNames); + break; + case Kind.FRAGMENT_SPREAD: { + const fragmentName = selection.name.value; + if (!visitedFragmentNames[fragmentName]) { + visitedFragmentNames[fragmentName] = true; + collectFields(fragments[fragmentName].selectionSet, fragments, fields, visitedFragmentNames); + } + break; + } + default: + // unreachable + break; + } + }); + } + + return fields; +} + +function aliasFieldNode(fieldNode: FieldNode, str: string): FieldNode { + return { + ...fieldNode, + alias: { + kind: Kind.NAME, + value: str, + }, + }; +} + +function hoistFieldNodes({ + fieldNode, + fieldNames, + path, + fragments, + context, + prefix, + index = 0, + wrappingPath = [], +}: { + fieldNode: FieldNode; + fieldNames?: Array; + path: Array; + fragments: Record; + context: WrapFieldsTransformationContext; + prefix: string; + index?: number; + wrappingPath?: ReadonlyArray; +}): Array { + const alias = fieldNode.alias != null ? fieldNode.alias.value : fieldNode.name.value; + + let newFieldNodes: Array = []; + + if (index < path.length) { + const pathSegment = path[index]; + collectFields(fieldNode.selectionSet, fragments).forEach((possibleFieldNode: FieldNode) => { + if (possibleFieldNode.name.value === pathSegment) { + const newWrappingPath = wrappingPath.concat([alias]); + + newFieldNodes = newFieldNodes.concat( + hoistFieldNodes({ + fieldNode: possibleFieldNode, + fieldNames, + path, + fragments, + context, + prefix, + index: index + 1, + wrappingPath: newWrappingPath, + }) + ); + } + }); + } else { + collectFields(fieldNode.selectionSet, fragments).forEach((possibleFieldNode: FieldNode) => { + if (!fieldNames || fieldNames.includes(possibleFieldNode.name.value)) { + const nextIndex = context.nextIndex; + context.nextIndex++; + const indexingAlias = `__${prefix}${nextIndex}__`; + context.paths[indexingAlias] = { + pathToField: wrappingPath.concat([alias]), + alias: possibleFieldNode.alias != null ? possibleFieldNode.alias.value : possibleFieldNode.name.value, + }; + newFieldNodes.push(aliasFieldNode(possibleFieldNode, indexingAlias)); + } + }); + } + + return newFieldNodes; +} + +export function dehoistValue(originalValue: any, context: WrapFieldsTransformationContext): any { + if (originalValue == null) { + return originalValue; + } + + const newValue = Object.create(null); + + Object.keys(originalValue).forEach(alias => { + let obj = newValue; + + const path = context.paths[alias]; + if (path == null) { + newValue[alias] = originalValue[alias]; + return; + } + + const pathToField = path.pathToField; + const fieldAlias = path.alias; + pathToField.forEach(key => { + obj = obj[key] = obj[key] || Object.create(null); + }); + obj[fieldAlias] = originalValue[alias]; + }); + + return newValue; +} + +function dehoistErrors( + errors: ReadonlyArray, + context: WrapFieldsTransformationContext +): Array { + if (errors === undefined) { + return undefined; + } + + return errors.map(error => { + const originalPath = error.path; + if (originalPath == null) { + return error; + } + + let newPath: Array = []; + originalPath.forEach(pathSegment => { + if (typeof pathSegment !== 'string') { + newPath.push(pathSegment); + return; + } + + const path = context.paths[pathSegment]; + if (path == null) { + newPath.push(pathSegment); + return; + } + + newPath = newPath.concat(path.pathToField, [path.alias]); + }); + + return relocatedError(error, newPath); + }); } 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; From 12fc7959628e7de94341e4b937ca0940c6e65c16 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sat, 4 Jul 2020 23:02:09 -0400 Subject: [PATCH 4/4] rename context to transformationContext --- packages/utils/src/Interfaces.ts | 2 +- packages/wrap/src/transforms/MapFields.ts | 8 ++++---- packages/wrap/src/transforms/WrapFields.ts | 20 ++++++++++++-------- packages/wrap/src/types.ts | 6 +++--- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/utils/src/Interfaces.ts b/packages/utils/src/Interfaces.ts index 82dee28c29c..6856f97e1e2 100644 --- a/packages/utils/src/Interfaces.ts +++ b/packages/utils/src/Interfaces.ts @@ -154,7 +154,7 @@ export interface Transform { export type FieldNodeMapper = ( fieldNode: FieldNode, fragments: Record, - context: Record + transformationContext: Record ) => SelectionNode | Array; export type FieldNodeMappers = Record>; diff --git a/packages/wrap/src/transforms/MapFields.ts b/packages/wrap/src/transforms/MapFields.ts index 190edb560ef..fe67ed53a25 100644 --- a/packages/wrap/src/transforms/MapFields.ts +++ b/packages/wrap/src/transforms/MapFields.ts @@ -16,7 +16,7 @@ export default class MapFields implements Transform { ) { this.transformer = new TransformCompositeFields( (_typeName, _fieldName, fieldConfig) => fieldConfig, - (typeName, fieldName, fieldNode, fragments, context) => { + (typeName, fieldName, fieldNode, fragments, transformationContext) => { const typeTransformers = fieldNodeTransformerMap[typeName]; if (typeTransformers == null) { return undefined; @@ -27,10 +27,10 @@ export default class MapFields implements Transform { return undefined; } - return fieldNodeTransformer(fieldNode, fragments, context); + return fieldNodeTransformer(fieldNode, fragments, transformationContext); }, objectValueTransformerMap != null - ? (data, context) => { + ? (data, transformationContext) => { if (data == null) { return data; } @@ -45,7 +45,7 @@ export default class MapFields implements Transform { return data; } - return transformer(data, context); + return transformer(data, transformationContext); } : undefined, errorsTransformer != null ? errorsTransformer : undefined diff --git a/packages/wrap/src/transforms/WrapFields.ts b/packages/wrap/src/transforms/WrapFields.ts index bc4fad8bcb4..ad5661e166d 100644 --- a/packages/wrap/src/transforms/WrapFields.ts +++ b/packages/wrap/src/transforms/WrapFields.ts @@ -71,13 +71,17 @@ export default class WrapFields implements Transform { this.transformer = new MapFields( { [outerTypeName]: { - [outerMostWrappingFieldName]: (fieldNode, fragments, context: WrapFieldsTransformationContext) => + [outerMostWrappingFieldName]: ( + fieldNode, + fragments, + transformationContext: WrapFieldsTransformationContext + ) => hoistFieldNodes({ fieldNode, path: remainingWrappingFieldNames, fieldNames, fragments, - context, + transformationContext, prefix, }), }, @@ -199,7 +203,7 @@ function hoistFieldNodes({ fieldNames, path, fragments, - context, + transformationContext, prefix, index = 0, wrappingPath = [], @@ -208,7 +212,7 @@ function hoistFieldNodes({ fieldNames?: Array; path: Array; fragments: Record; - context: WrapFieldsTransformationContext; + transformationContext: WrapFieldsTransformationContext; prefix: string; index?: number; wrappingPath?: ReadonlyArray; @@ -229,7 +233,7 @@ function hoistFieldNodes({ fieldNames, path, fragments, - context, + transformationContext, prefix, index: index + 1, wrappingPath: newWrappingPath, @@ -240,10 +244,10 @@ function hoistFieldNodes({ } else { collectFields(fieldNode.selectionSet, fragments).forEach((possibleFieldNode: FieldNode) => { if (!fieldNames || fieldNames.includes(possibleFieldNode.name.value)) { - const nextIndex = context.nextIndex; - context.nextIndex++; + const nextIndex = transformationContext.nextIndex; + transformationContext.nextIndex++; const indexingAlias = `__${prefix}${nextIndex}__`; - context.paths[indexingAlias] = { + transformationContext.paths[indexingAlias] = { pathToField: wrappingPath.concat([alias]), alias: possibleFieldNode.alias != null ? possibleFieldNode.alias.value : possibleFieldNode.name.value, }; diff --git a/packages/wrap/src/types.ts b/packages/wrap/src/types.ts index 330ce1ba506..95176754351 100644 --- a/packages/wrap/src/types.ts +++ b/packages/wrap/src/types.ts @@ -60,14 +60,14 @@ export type FieldNodeTransformer = ( fieldName: string, fieldNode: FieldNode, fragments: Record, - context: Record + transformationContext: Record ) => SelectionNode | Array; -export type DataTransformer = (value: any, context?: Record) => any; +export type DataTransformer = (value: any, transformationContext?: Record) => any; export type ObjectValueTransformerMap = Record; export type ErrorsTransformer = ( errors: ReadonlyArray, - context: Record + transformationContext: Record ) => Array;