From 6f5f37d2c07c26ca98fa1a6053426971715d1633 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 4 Jun 2020 06:10:08 -0400 Subject: [PATCH] add FilterInputObjectFields, RenameInputObjectFields closes #1058 --- packages/utils/src/Interfaces.ts | 6 ++ .../src/transforms/FilterInputObjectFields.ts | 28 ++++++++ .../src/transforms/RenameInputObjectFields.ts | 72 +++++++++++++++++++ packages/wrap/src/transforms/index.ts | 3 + packages/wrap/tests/transforms.test.ts | 71 ++++++++++++++---- 5 files changed, 168 insertions(+), 12 deletions(-) create mode 100644 packages/wrap/src/transforms/FilterInputObjectFields.ts create mode 100644 packages/wrap/src/transforms/RenameInputObjectFields.ts diff --git a/packages/utils/src/Interfaces.ts b/packages/utils/src/Interfaces.ts index c4be11147b5..a03fee5e7a6 100644 --- a/packages/utils/src/Interfaces.ts +++ b/packages/utils/src/Interfaces.ts @@ -158,6 +158,12 @@ export type FieldNodeMapper = ( export type FieldNodeMappers = Record>; +export type InputFieldFilter = ( + typeName?: string, + fieldName?: string, + inputFieldConfig?: GraphQLInputFieldConfig +) => boolean; + export type FieldFilter = ( typeName?: string, fieldName?: string, diff --git a/packages/wrap/src/transforms/FilterInputObjectFields.ts b/packages/wrap/src/transforms/FilterInputObjectFields.ts new file mode 100644 index 00000000000..a22476b0e5c --- /dev/null +++ b/packages/wrap/src/transforms/FilterInputObjectFields.ts @@ -0,0 +1,28 @@ +import { GraphQLSchema, GraphQLInputFieldConfig } from 'graphql'; + +import { Transform, Request, InputFieldFilter } from '@graphql-tools/utils'; + +import TransformInputFields from './TransformInputFields'; +import { DelegationContext } from 'packages/delegate/src'; +import { InputObjectNodeTransformer } from '../types'; + +export default class FilterInputObjectFields implements Transform { + private readonly transformer: TransformInputFields; + + constructor(filter: InputFieldFilter, inputObjectNodeTransformer: InputObjectNodeTransformer) { + this.transformer = new TransformInputFields( + (typeName: string, fieldName: string, inputFieldConfig: GraphQLInputFieldConfig) => + filter(typeName, fieldName, inputFieldConfig) ? undefined : null, + undefined, + inputObjectNodeTransformer + ); + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return this.transformer.transformSchema(originalSchema); + } + + public transformRequest(originalRequest: Request, delegationContext: DelegationContext): Request { + return this.transformer.transformRequest(originalRequest, delegationContext); + } +} diff --git a/packages/wrap/src/transforms/RenameInputObjectFields.ts b/packages/wrap/src/transforms/RenameInputObjectFields.ts new file mode 100644 index 00000000000..8034d007d18 --- /dev/null +++ b/packages/wrap/src/transforms/RenameInputObjectFields.ts @@ -0,0 +1,72 @@ +import { GraphQLSchema, GraphQLInputFieldConfig, ObjectFieldNode } from 'graphql'; + +import { Transform, Request, mapSchema, MapperKind } from '@graphql-tools/utils'; + +import TransformInputFields from './TransformInputFields'; +import { DelegationContext } from 'packages/delegate/src'; + +export default class RenameInputObjectFields implements Transform { + private readonly renamer: (typeName: string, fieldName: string, inputFieldConfig: GraphQLInputFieldConfig) => string; + private readonly transformer: TransformInputFields; + private reverseMap: Record>; + + constructor(renamer: (typeName: string, fieldName: string, inputFieldConfig: GraphQLInputFieldConfig) => string) { + this.renamer = renamer; + this.transformer = new TransformInputFields( + (typeName: string, inputFieldName: string, inputFieldConfig: GraphQLInputFieldConfig) => { + const newName = renamer(typeName, inputFieldName, inputFieldConfig); + if (newName !== undefined && newName !== inputFieldName) { + return [renamer(typeName, inputFieldName, inputFieldConfig), inputFieldConfig]; + } + }, + (typeName: string, inputFieldName: string, inputFieldNode: ObjectFieldNode) => { + if (!(typeName in this.reverseMap)) { + return inputFieldNode; + } + + const inputFieldNameMap = this.reverseMap[typeName]; + if (!(inputFieldName in inputFieldNameMap)) { + return inputFieldNode; + } + + return { + ...inputFieldNode, + name: { + ...inputFieldNode.name, + value: inputFieldNameMap[inputFieldName], + }, + }; + } + ); + this.reverseMap = Object.create(null); + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + mapSchema(originalSchema, { + [MapperKind.INPUT_OBJECT_FIELD]: ( + inputFieldConfig: GraphQLInputFieldConfig, + fieldName: string, + typeName + ): undefined => { + const newName = this.renamer(typeName, fieldName, inputFieldConfig); + if (newName !== undefined && newName !== fieldName) { + if (this.reverseMap[typeName] == null) { + this.reverseMap[typeName] = Object.create(null); + } + this.reverseMap[typeName][newName] = fieldName; + } + return undefined; + }, + + [MapperKind.ROOT_OBJECT]() { + return undefined; + }, + }); + + return this.transformer.transformSchema(originalSchema); + } + + public transformRequest(originalRequest: Request, delegationContext: DelegationContext): Request { + return this.transformer.transformRequest(originalRequest, delegationContext); + } +} diff --git a/packages/wrap/src/transforms/index.ts b/packages/wrap/src/transforms/index.ts index 9f620acbc5e..5143b827f01 100644 --- a/packages/wrap/src/transforms/index.ts +++ b/packages/wrap/src/transforms/index.ts @@ -11,6 +11,9 @@ export { default as FilterObjectFields } from './FilterObjectFields'; export { default as TransformInterfaceFields } from './TransformInterfaceFields'; export { default as RenameInterfaceFields } from './RenameInterfaceFields'; export { default as FilterInterfaceFields } from './FilterInterfaceFields'; +export { default as TransformInputFields } from './TransformInputFields'; +export { default as RenameInputObjectFields } from './RenameInputObjectFields'; +export { default as FilterInputObjectFields } from './FilterInputObjectFields'; export { default as TransformQuery } from './TransformQuery'; export { default as ExtendSchema } from './ExtendSchema'; diff --git a/packages/wrap/tests/transforms.test.ts b/packages/wrap/tests/transforms.test.ts index 8600d408b7d..598dc74e54a 100644 --- a/packages/wrap/tests/transforms.test.ts +++ b/packages/wrap/tests/transforms.test.ts @@ -7,8 +7,8 @@ import { SelectionSetNode, print, parse, - GraphQLInputObjectType, astFromValue, + GraphQLString, } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -26,6 +26,8 @@ import { WrapQuery, ExtractField, TransformQuery, + FilterInputObjectFields, + RenameInputObjectFields, } from '@graphql-tools/wrap'; import { @@ -37,7 +39,6 @@ import { } from '@graphql-tools/delegate'; import { propertySchema, bookingSchema } from './fixtures/schemas'; -import TransformInputFields from '../src/transforms/TransformInputFields'; function createError(message: string, extra?: T) { const error = new Error(message); @@ -1372,8 +1373,8 @@ describe('replaces field with processed fragment node', () => { }); }); - describe('transform input type', () => { - test('it works', async () => { + describe('transform input object fields', () => { + test('filtering works', async () => { const schema = makeExecutableSchema({ typeDefs: ` input InputObject { @@ -1400,13 +1401,8 @@ describe('replaces field with processed fragment node', () => { }); const transformedSchema = wrapSchema(schema, [ - new TransformInputFields( - (typeName, fieldName) => { - if (typeName === 'InputObject' && fieldName === 'field2') { - return null; - } - }, - undefined, + new FilterInputObjectFields( + (typeName, fieldName) => (typeName !== 'InputObject' || fieldName !== 'field2'), (typeName, inputObjectNode) => { if (typeName === 'InputObject') { return { @@ -1417,7 +1413,7 @@ describe('replaces field with processed fragment node', () => { kind: Kind.NAME, value: 'field2', }, - value: astFromValue('field2', (schema.getType('InputObject') as GraphQLInputObjectType).getFields()['field2'].type), + value: astFromValue('field2', GraphQLString), }], }; } @@ -1439,4 +1435,55 @@ describe('replaces field with processed fragment node', () => { expect(result.data.test.field2).toBe('field2'); }); }); + + test('renaming works', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + input InputObject { + field1: String + field2: String + } + + type OutputObject { + field1: String + field2: String + } + + type Query { + test(argument: InputObject): OutputObject + } + `, + resolvers: { + Query: { + test: (_root, args) => { + return args.argument; + } + } + } + }); + + const transformedSchema = wrapSchema(schema, [ + new RenameInputObjectFields( + (typeName: string, fieldName: string) => { + if (typeName === 'InputObject' && fieldName === 'field2') { + return 'field3'; + } + }, + ) + ]); + + const query = `{ + test(argument: { + field1: "field1" + field3: "field2" + }) { + field1 + field2 + } + }`; + + const result = await graphql(transformedSchema, query); + expect(result.data.test.field1).toBe('field1'); + expect(result.data.test.field2).toBe('field2'); + }); });