Skip to content

Commit

Permalink
introduce input object field transformers: Filter/Rename/TransformInp…
Browse files Browse the repository at this point in the history
…utObjectFields (#1551)
  • Loading branch information
yaacovCR committed Jun 5, 2020
1 parent 001dce6 commit 35fd769
Show file tree
Hide file tree
Showing 7 changed files with 447 additions and 1 deletion.
6 changes: 6 additions & 0 deletions packages/utils/src/Interfaces.ts
Expand Up @@ -158,6 +158,12 @@ export type FieldNodeMapper = (

export type FieldNodeMappers = Record<string, Record<string, FieldNodeMapper>>;

export type InputFieldFilter = (
typeName?: string,
fieldName?: string,
inputFieldConfig?: GraphQLInputFieldConfig
) => boolean;

export type FieldFilter = (
typeName?: string,
fieldName?: string,
Expand Down
28 changes: 28 additions & 0 deletions packages/wrap/src/transforms/FilterInputObjectFields.ts
@@ -0,0 +1,28 @@
import { GraphQLSchema, GraphQLInputFieldConfig } from 'graphql';

import { Transform, Request, InputFieldFilter } from '@graphql-tools/utils';

import TransformInputObjectFields from './TransformInputObjectFields';
import { DelegationContext } from 'packages/delegate/src';
import { InputObjectNodeTransformer } from '../types';

export default class FilterInputObjectFields implements Transform {
private readonly transformer: TransformInputObjectFields;

constructor(filter: InputFieldFilter, inputObjectNodeTransformer?: InputObjectNodeTransformer) {
this.transformer = new TransformInputObjectFields(
(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);
}
}
72 changes: 72 additions & 0 deletions 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 TransformInputObjectFields from './TransformInputObjectFields';
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: TransformInputObjectFields;
private reverseMap: Record<string, Record<string, string>>;

constructor(renamer: (typeName: string, fieldName: string, inputFieldConfig: GraphQLInputFieldConfig) => string) {
this.renamer = renamer;
this.transformer = new TransformInputObjectFields(
(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);
}
}
194 changes: 194 additions & 0 deletions packages/wrap/src/transforms/TransformInputObjectFields.ts
@@ -0,0 +1,194 @@
import {
GraphQLSchema,
GraphQLType,
DocumentNode,
TypeInfo,
visit,
visitWithTypeInfo,
Kind,
FragmentDefinitionNode,
GraphQLInputObjectType,
ObjectValueNode,
ObjectFieldNode,
} from 'graphql';

import { Transform, Request, MapperKind, mapSchema } from '@graphql-tools/utils';
import { InputFieldTransformer, InputFieldNodeTransformer, InputObjectNodeTransformer } from '../types';
import { DelegationContext } from 'packages/delegate/src';

export default class TransformInputObjectFields implements Transform {
private readonly inputFieldTransformer: InputFieldTransformer;
private readonly inputFieldNodeTransformer: InputFieldNodeTransformer;
private readonly inputObjectNodeTransformer: InputObjectNodeTransformer;
private transformedSchema: GraphQLSchema;
private mapping: Record<string, Record<string, string>>;

constructor(
inputFieldTransformer: InputFieldTransformer,
inputFieldNodeTransformer?: InputFieldNodeTransformer,
inputObjectNodeTransformer?: InputObjectNodeTransformer
) {
this.inputFieldTransformer = inputFieldTransformer;
this.inputFieldNodeTransformer = inputFieldNodeTransformer;
this.inputObjectNodeTransformer = inputObjectNodeTransformer;
this.mapping = {};
}

public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema {
this.transformedSchema = mapSchema(originalSchema, {
[MapperKind.INPUT_OBJECT_TYPE]: (type: GraphQLInputObjectType) =>
this.transformFields(type, this.inputFieldTransformer),
});

return this.transformedSchema;
}

public transformRequest(originalRequest: Request, delegationContext: DelegationContext): 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.inputFieldNodeTransformer,
this.inputObjectNodeTransformer,
delegationContext,
originalRequest
);
return {
...originalRequest,
document,
};
}

private transformFields(type: GraphQLInputObjectType, inputFieldTransformer: InputFieldTransformer): any {
const config = type.toConfig();

const originalInputFieldConfigMap = config.fields;
const newInputFieldConfigMap = {};

Object.keys(originalInputFieldConfigMap).forEach(fieldName => {
const originalInputFieldConfig = originalInputFieldConfigMap[fieldName];
const transformedField = inputFieldTransformer(type.name, fieldName, originalInputFieldConfig);

if (transformedField === undefined) {
newInputFieldConfigMap[fieldName] = originalInputFieldConfig;
} else if (Array.isArray(transformedField)) {
const newFieldName = transformedField[0];
const newFieldConfig = transformedField[1];
newInputFieldConfigMap[newFieldName] = newFieldConfig;

if (newFieldName !== fieldName) {
const typeName = type.name;
if (!(typeName in this.mapping)) {
this.mapping[typeName] = {};
}
this.mapping[typeName][newFieldName] = fieldName;
}
} else if (transformedField != null) {
newInputFieldConfigMap[fieldName] = transformedField;
}
});

if (!Object.keys(newInputFieldConfigMap).length) {
return null;
}

return new GraphQLInputObjectType({
...type.toConfig(),
fields: newInputFieldConfigMap,
});
}

private transformDocument(
document: DocumentNode,
mapping: Record<string, Record<string, string>>,
inputFieldNodeTransformer: InputFieldNodeTransformer,
inputObjectNodeTransformer: InputObjectNodeTransformer,
delegationContext: DelegationContext,
request: Request
): DocumentNode {
const typeInfo = new TypeInfo(this.transformedSchema);
const newDocument: DocumentNode = visit(
document,
visitWithTypeInfo(typeInfo, {
leave: {
[Kind.OBJECT]: (node: ObjectValueNode): ObjectValueNode => {
const parentType: GraphQLType = typeInfo.getInputType() as GraphQLInputObjectType;
if (parentType != null) {
const parentTypeName = parentType.name;
const newInputFields: Array<ObjectFieldNode> = [];

node.fields.forEach(inputField => {
const newName = inputField.name.value;

const transformedInputField =
inputFieldNodeTransformer != null
? inputFieldNodeTransformer(parentTypeName, newName, inputField, delegationContext, request)
: inputField;

if (Array.isArray(transformedInputField)) {
transformedInputField.forEach(individualTransformedInputField => {
const typeMapping = mapping[parentTypeName];
if (typeMapping == null) {
newInputFields.push(individualTransformedInputField);
return;
}

const oldName = typeMapping[newName];
if (oldName == null) {
newInputFields.push(individualTransformedInputField);
return;
}

newInputFields.push({
...individualTransformedInputField,
name: {
...individualTransformedInputField.name,
value: oldName,
},
});
});
return;
}

const typeMapping = mapping[parentTypeName];
if (typeMapping == null) {
newInputFields.push(transformedInputField);
return;
}

const oldName = typeMapping[newName];
if (oldName == null) {
newInputFields.push(transformedInputField);
return;
}

newInputFields.push({
...transformedInputField,
name: {
...transformedInputField.name,
value: oldName,
},
});
});

const newNode = {
...node,
fields: newInputFields,
};

return inputObjectNodeTransformer != null
? inputObjectNodeTransformer(parentTypeName, newNode, delegationContext, request)
: newNode;
}
},
},
})
);
return newDocument;
}
}
3 changes: 3 additions & 0 deletions packages/wrap/src/transforms/index.ts
Expand Up @@ -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 TransformInputObjectFields } from './TransformInputObjectFields';
export { default as RenameInputObjectFields } from './RenameInputObjectFields';
export { default as FilterInputObjectFields } from './FilterInputObjectFields';
export { default as TransformQuery } from './TransformQuery';

export { default as ExtendSchema } from './ExtendSchema';
Expand Down
27 changes: 26 additions & 1 deletion packages/wrap/src/types.ts
Expand Up @@ -2,12 +2,16 @@ import {
GraphQLSchema,
GraphQLFieldResolver,
BuildSchemaOptions,
GraphQLInputFieldConfig,
GraphQLFieldConfig,
FieldNode,
FragmentDefinitionNode,
SelectionNode,
ObjectFieldNode,
ObjectValueNode,
} from 'graphql';
import { Executor, Subscriber } from '@graphql-tools/delegate';
import { Executor, Subscriber, DelegationContext } from '@graphql-tools/delegate';
import { Request } from '@graphql-tools/utils';

export interface IMakeRemoteExecutableSchemaOptions {
schema: GraphQLSchema | string;
Expand All @@ -17,6 +21,27 @@ export interface IMakeRemoteExecutableSchemaOptions {
buildSchemaOptions?: BuildSchemaOptions;
}

export type InputFieldTransformer = (
typeName: string,
fieldName: string,
inputFieldConfig: GraphQLInputFieldConfig
) => GraphQLInputFieldConfig | [string, GraphQLInputFieldConfig] | null | undefined;

export type InputFieldNodeTransformer = (
typeName: string,
fieldName: string,
inputFieldNode: ObjectFieldNode,
delegationContext: DelegationContext,
request: Request
) => ObjectFieldNode | Array<ObjectFieldNode>;

export type InputObjectNodeTransformer = (
typeName: string,
inputObjectNode: ObjectValueNode,
delegationContext: DelegationContext,
request: Request
) => ObjectValueNode;

export type FieldTransformer = (
typeName: string,
fieldName: string,
Expand Down

0 comments on commit 35fd769

Please sign in to comment.