Skip to content
This repository has been archived by the owner on Apr 15, 2020. It is now read-only.

Commit

Permalink
feat(transforms): add HoistField transform
Browse files Browse the repository at this point in the history
New HoistField transform allows moving a field on a remote type into a higher level object within the wrapping schema.

The appendFields and filterFields methods from the WrapFIelds transform have been moved to the utils directory and exported; filterFields has been renamed to removeFields to better describe what it does.
  • Loading branch information
yaacovCR committed Jan 6, 2020
1 parent 15f40d6 commit becf901
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 69 deletions.
39 changes: 33 additions & 6 deletions src/test/testAlternateMergeSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ExtendSchema,
WrapType,
WrapFields,
HoistField,
FilterRootFields,
FilterObjectFields,
} from '../transforms';
Expand Down Expand Up @@ -867,10 +868,8 @@ type Wrap {
});

describe('schema transformation with extraction of nested fields', () => {
let transformedPropertySchema: GraphQLSchema;

before(async () => {
transformedPropertySchema = transformSchema(propertySchema, [
it('should work via ExtendSchema transform', async () => {
const transformedPropertySchema = transformSchema(propertySchema, [
new ExtendSchema({
typeDefs: `
extend type Property {
Expand All @@ -892,9 +891,7 @@ describe('schema transformation with extraction of nested fields', () => {
},
}),
]);
});

it('should work to extract a field', async () => {
const result = await graphql(
transformedPropertySchema,
`
Expand Down Expand Up @@ -943,6 +940,36 @@ describe('schema transformation with extraction of nested fields', () => {
]
});
});

it('should work via HoistField transform', async () => {
const transformedPropertySchema = transformSchema(propertySchema, [
new HoistField('Property', ['location', 'name'], 'locationName'),
]);

const result = await graphql(
transformedPropertySchema,
`
query($pid: ID!) {
propertyById(id: $pid) {
test: locationName
}
}
`,
{},
{},
{
pid: 'p1',
},
);

expect(result).to.deep.equal({
data: {
propertyById: {
test: 'Helsinki',
},
},
});
});
});

describe('schema transformation with wrapping of object fields', () => {
Expand Down
80 changes: 80 additions & 0 deletions src/transforms/HoistField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* tslint:disable:no-unused-expression */

import {
GraphQLSchema,
GraphQLObjectType,
getNullableType,
} from 'graphql';
import { Request } from '../Interfaces';
import { Transform } from './transforms';
import { healSchema, wrapFieldNode, renameFieldNode } from '../utils';
import { createMergedResolver } from '../stitching';
import { default as MapFields } from './MapFields';
import { appendFields, removeFields } from '../utils/fields';

export default class HoistField implements Transform {
private typeName: string;
private path: Array<string>;
private newFieldName: string;
private pathToField: Array<string>;
private oldFieldName: string;
private delimeter: string;
private transformer: Transform;

constructor(
typeName: string,
path: Array<string>,
newFieldName: string,
delimeter: string = '__gqltf__',
) {
this.typeName = typeName;
this.path = path;
this.newFieldName = newFieldName;
this.delimeter = delimeter;

this.pathToField = this.path.slice();
this.oldFieldName = this.pathToField.pop();
this.transformer = new MapFields({
[typeName]: {
[newFieldName]: fieldNode => wrapFieldNode(
renameFieldNode(fieldNode, this.oldFieldName),
this.pathToField,
this.delimeter
),
},
});
}

public transformSchema(schema: GraphQLSchema): GraphQLSchema {
const typeMap = schema.getTypeMap();

const innerType: GraphQLObjectType<any, any> = this.pathToField.reduce(
(acc, pathSegment) =>
getNullableType(acc.getFields()[pathSegment].type) as GraphQLObjectType<any, any>,
typeMap[this.typeName] as GraphQLObjectType<any, any>
);

const targetField = removeFields(
typeMap,
innerType.name,
fieldName => fieldName === this.oldFieldName,
)[this.oldFieldName];

const targetType = (targetField.type as GraphQLObjectType<any, any>);

appendFields(typeMap, this.typeName, {
[this.newFieldName]: {
type: targetType,
resolve: createMergedResolver({ fromPath: this.pathToField, delimeter: this.delimeter }),
},
});

healSchema(schema);

return this.transformer.transformSchema(schema);
}

public transformRequest(originalRequest: Request): Request {
return this.transformer.transformRequest(originalRequest);
}
}
68 changes: 5 additions & 63 deletions src/transforms/WrapFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLObjectTypeConfig,
GraphQLFieldConfigMap,
GraphQLFieldConfig,
} from 'graphql';
import { Request } from '../Interfaces';
import { Transform } from './transforms';
import { hoistFieldNodes, healSchema } from '../utils';
import { defaultMergedResolver, createMergedResolver } from '../stitching';
import { default as MapFields } from './MapFields';
import { TypeMap } from 'graphql/type/schema';
import { appendFields, removeFields } from '../utils/fields';

export default class WrapFields implements Transform {
private outerTypeName: string;
Expand Down Expand Up @@ -55,7 +52,7 @@ export default class WrapFields implements Transform {
public transformSchema(schema: GraphQLSchema): GraphQLSchema {
const typeMap = schema.getTypeMap();

const targetFields = this.filterFields(
const targetFields = removeFields(
typeMap,
this.outerTypeName,
!this.fieldNames ? () => true : fieldName => this.fieldNames.includes(fieldName)
Expand All @@ -64,18 +61,18 @@ export default class WrapFields implements Transform {
let wrapIndex = this.numWraps - 1;

const innerMostWrappingTypeName = this.wrappingTypeNames[wrapIndex];
this.appendFields(typeMap, innerMostWrappingTypeName, targetFields);
appendFields(typeMap, innerMostWrappingTypeName, targetFields);

for (wrapIndex--; wrapIndex > -1; wrapIndex--) {
this.appendFields(typeMap, this.wrappingTypeNames[wrapIndex], {
appendFields(typeMap, this.wrappingTypeNames[wrapIndex], {
[this.wrappingFieldNames[wrapIndex + 1]]: {
type: typeMap[this.wrappingTypeNames[wrapIndex + 1]] as GraphQLObjectType,
resolve: defaultMergedResolver,
}
});
}

this.appendFields(typeMap, this.outerTypeName, {
appendFields(typeMap, this.outerTypeName, {
[this.wrappingFieldNames[0]]: {
type: typeMap[this.wrappingTypeNames[0]] as GraphQLObjectType,
resolve: createMergedResolver({ dehoist: true, delimeter: this.delimeter }),
Expand All @@ -90,59 +87,4 @@ export default class WrapFields implements Transform {
public transformRequest(originalRequest: Request): Request {
return this.transformer.transformRequest(originalRequest);
}

private appendFields(
typeMap: TypeMap,
typeName: string,
fields: GraphQLFieldConfigMap<any, any>,
) {
let type = typeMap[typeName];
if (type) {
const typeConfig = type.toConfig() as GraphQLObjectTypeConfig<any, any>;
const originalFields = typeConfig.fields;
const newFields = {};
Object.keys(originalFields).forEach(fieldName => {
newFields[fieldName] = originalFields[fieldName];
});
Object.keys(fields).forEach(fieldName => {
newFields[fieldName] = fields[fieldName];
});
type = new GraphQLObjectType({
...typeConfig,
fields: newFields,
});
} else {
type = new GraphQLObjectType({
name: typeName,
fields,
});
}
typeMap[typeName] = type;
}

private filterFields(
typeMap: TypeMap,
typeName: string,
filter: (fieldName: string, field: GraphQLFieldConfig<any, any>) => boolean,
): GraphQLFieldConfigMap<any, any> {
let type = typeMap[typeName];
const typeConfig = type.toConfig() as GraphQLObjectTypeConfig<any, any>;
const originalFields = typeConfig.fields;
const newFields = {};
const filteredFields = {};
Object.keys(originalFields).forEach(fieldName => {
if (filter(fieldName, originalFields[fieldName])) {
filteredFields[fieldName] = originalFields[fieldName];
} else {
newFields[fieldName] = originalFields[fieldName];
}
});
type = new GraphQLObjectType({
...typeConfig,
fields: newFields,
});
typeMap[typeName] = type;

return filteredFields;
}
}
1 change: 1 addition & 0 deletions src/transforms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ export { default as TransformQuery } from './TransformQuery';
export { default as ExtendSchema } from './ExtendSchema';
export { default as WrapType } from './WrapType';
export { default as WrapFields } from './WrapFields';
export { default as HoistField } from './HoistField';
export { default as MapFields } from './MapFields';
export { default as ReplaceFieldWithFragment } from './ReplaceFieldWithFragment';
62 changes: 62 additions & 0 deletions src/utils/fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
GraphQLFieldConfigMap,
GraphQLObjectTypeConfig,
GraphQLObjectType,
GraphQLFieldConfig,
} from 'graphql';
import { TypeMap } from 'graphql/type/schema';

export function appendFields(
typeMap: TypeMap,
typeName: string,
fields: GraphQLFieldConfigMap<any, any>,
): void {
let type = typeMap[typeName];
if (type) {
const typeConfig = type.toConfig() as GraphQLObjectTypeConfig<any, any>;
const originalFields = typeConfig.fields;
const newFields = {};
Object.keys(originalFields).forEach(fieldName => {
newFields[fieldName] = originalFields[fieldName];
});
Object.keys(fields).forEach(fieldName => {
newFields[fieldName] = fields[fieldName];
});
type = new GraphQLObjectType({
...typeConfig,
fields: newFields,
});
} else {
type = new GraphQLObjectType({
name: typeName,
fields,
});
}
typeMap[typeName] = type;
}

export function removeFields(
typeMap: TypeMap,
typeName: string,
testFn: (fieldName: string, field: GraphQLFieldConfig<any, any>) => boolean,
): GraphQLFieldConfigMap<any, any> {
let type = typeMap[typeName];
const typeConfig = type.toConfig() as GraphQLObjectTypeConfig<any, any>;
const originalFields = typeConfig.fields;
const newFields = {};
const removedFields = {};
Object.keys(originalFields).forEach(fieldName => {
if (testFn(fieldName, originalFields[fieldName])) {
removedFields[fieldName] = originalFields[fieldName];
} else {
newFields[fieldName] = originalFields[fieldName];
}
});
type = new GraphQLObjectType({
...typeConfig,
fields: newFields,
});
typeMap[typeName] = type;

return removedFields;
}
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export {
renameFieldNode,
hoistFieldNodes,
} from './fieldNodes';
export { appendFields, removeFields } from './fields';

0 comments on commit becf901

Please sign in to comment.