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

Commit

Permalink
[feat] Add transformers to rename, filter, and arbitrarily transform …
Browse files Browse the repository at this point in the history
…object fields, fixes ardatan#819.
  • Loading branch information
yaacovCR committed Sep 22, 2019
1 parent 66aa82b commit 8f569e1
Show file tree
Hide file tree
Showing 7 changed files with 399 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
the use of the `RenameRootFields` transform with subscriptions,
pull request [#1104](https://github.com/apollographql/graphql-tools/pull/1104), fixes
[#997](https://github.com/apollographql/graphql-tools/issues/997). <br/>
* Add transformers to rename, filter, and arbitrarily transform object fields. <br/>
Fixes [#819](https://github.com/apollographql/graphql-tools/issues/819).

### 4.0.4

Expand Down
48 changes: 48 additions & 0 deletions docs/source/schema-transforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,54 @@ RenameRootFields(
)
```

### Modifying object fields

* `TransformObjectFields(objectFieldTransformer: ObjectFieldTransformer, fieldNodeTransformer?: FieldNodeTransformer))`: Given an object field transformer, arbitrarily transform fields. The `objectFieldTransformer` can return a `GraphQLFieldConfig` definition, a object with new `name` and a `field`, `null` to remove the field, or `undefined` to leave the field unchanged. The optional `fieldNodeTransformer`, if specified, is called upon any field of that type in the request; result transformation can be specified by wrapping the resolve function within the `objectFieldTransformer`. In this way, a field can be fully arbitrarily modified in place.

```ts
TransformObjectFields(objectFieldTransformer: ObjectFieldTransformer, fieldNodeTransformer: FieldNodeTransformer)

type ObjectFieldTransformer = (
typeName: string,
fieldName: string,
field: GraphQLField<any, any>,
) =>
| GraphQLFieldConfig<any, any>
| { name: string; field: GraphQLFieldConfig<any, any> }
| null
| void;

type FieldNodeTransformer = (
typeName: string,
fieldName: string,
fieldNode: FieldNode
) => FieldNode;
```

* `FilterObjectFields(filter: ObjectFilter)`: Removes object fields for which the `filter` function returns `false`.

```ts
FilterObjectFields(filter: ObjectFilter)

type ObjectFilter = (
typeName: string,
fieldName: string,
field: GraphQLField<any, any>,
) => boolean;
```

* `RenameObjectFields(renamer)`: Rename object fields, by applying the `renamer` function to their names.

```ts
RenameObjectFields(
renamer: (
typeName: string,
fieldName: string,
field: GraphQLField<any, any>,
) => string,
)
```

### Other

* `ExtractField({ from: Array<string>, to: Array<string> })` - move selection at `from` path to `to` path.
Expand Down
128 changes: 124 additions & 4 deletions src/test/testAlternateMergeSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import {
ExecutionResult,
subscribe,
parse,
GraphQLField,
GraphQLNamedType,
FieldNode
} from 'graphql';
import mergeSchemas from '../stitching/mergeSchemas';
import {
transformSchema,
FilterRootFields,
RenameTypes,
RenameRootFields,
RenameObjectFields,
FilterObjectFields,
TransformObjectFields,
} from '../transforms';
import {
propertySchema,
Expand All @@ -23,6 +29,7 @@ import {
subscriptionPubSubTrigger,
} from './testingSchemas';
import { forAwaitEach } from 'iterall';
import { createResolveType, fieldToFieldConfig } from '../stitching/schemaRecreation';

let linkSchema = `
"""
Expand Down Expand Up @@ -79,17 +86,15 @@ describe('merge schemas through transforms', () => {
'Query.properties' === `${operation}.${rootField}`,
),
new RenameTypes((name: string) => `Properties_${name}`),
new RenameRootFields((name: string) => `Properties_${name}`),
new RenameRootFields((operation: string, name: string) => `Properties_${name}`),
]);
const transformedBookingSchema = transformSchema(bookingSchema, [
new FilterRootFields(
(operation: string, rootField: string) =>
'Query.bookings' === `${operation}.${rootField}`,
),
new RenameTypes((name: string) => `Bookings_${name}`),
new RenameRootFields(
(operation: string, name: string) => `Bookings_${name}`,
),
new RenameRootFields((operation: string, name: string) => `Bookings_${name}`),
]);
const transformedSubscriptionSchema = transformSchema(subscriptionSchema, [
new FilterRootFields(
Expand Down Expand Up @@ -296,6 +301,121 @@ describe('merge schemas through transforms', () => {
});
});

describe('transform object fields', () => {
let transformedPropertySchema: GraphQLSchema;

before(async () => {
const resolveType = createResolveType((name: string, type: GraphQLNamedType): GraphQLNamedType => type);
transformedPropertySchema = transformSchema(propertySchema, [
new TransformObjectFields(
(typeName: string, fieldName: string, field: GraphQLField<any, any>) => {
const fieldConfig = fieldToFieldConfig(field, resolveType, true);
if (typeName !== 'Property' || fieldName !== 'name') {
return fieldConfig;
}
fieldConfig.resolve = () => 'test';
return fieldConfig;
},
(typeName: string, fieldName: string, fieldNode: FieldNode) => {
if (typeName !== 'Property' || fieldName !== 'name') {
return fieldNode;
}
const newFieldNode = {
...fieldNode,
name: {
...fieldNode.name,
value: 'id'
}
};
return newFieldNode;
}
)
]);
});

it('should work', async () => {
const result = await graphql(
transformedPropertySchema,
`
query($pid: ID!) {
propertyById(id: $pid) {
id
name
location {
name
}
}
}
`,
{},
{},
{
pid: 'p1',
},
);

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

describe('filter and rename object fields', () => {
let transformedPropertySchema: GraphQLSchema;

before(async () => {
transformedPropertySchema = transformSchema(propertySchema, [
new RenameTypes((name: string) => `New_${name}`),
new FilterObjectFields((typeName: string, fieldName: string) =>
(typeName !== 'NewProperty' || fieldName === 'id' || fieldName === 'name' || fieldName === 'location')
),
new RenameObjectFields((typeName: string, fieldName: string) => (typeName === 'New_Property' ? `new_${fieldName}` : fieldName))
]);
});

it('should work', async () => {
const result = await graphql(
transformedPropertySchema,
`
query($pid: ID!) {
propertyById(id: $pid) {
new_id
new_name
new_location {
name
}
}
}
`,
{},
{},
{
pid: 'p1',
},
);

expect(result).to.deep.equal({
data: {
propertyById: {
new_id: 'p1',
new_name: 'Super great hotel',
new_location: {
name: 'Helsinki',
},
},
},
});
});
});

describe('interface resolver inheritance', () => {
const testSchemaWithInterfaceResolvers = `
interface Node {
Expand Down
25 changes: 25 additions & 0 deletions src/transforms/FilterObjectFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { GraphQLField, GraphQLSchema } from 'graphql';
import { Transform } from './transforms';
import TransformObjectFields from './TransformObjectFields';

export type ObjectFilter = (typeName: string, fieldName: string, field: GraphQLField<any, any>) => boolean;

export default class FilterObjectFields implements Transform {
private transformer: TransformObjectFields;

constructor(filter: ObjectFilter) {
this.transformer = new TransformObjectFields(
(typeName: string, fieldName: string, field: GraphQLField<any, any>) => {
if (filter(typeName, fieldName, field)) {
return undefined;
} else {
return null;
}
}
);
}

public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema {
return this.transformer.transformSchema(originalSchema);
}
}
29 changes: 29 additions & 0 deletions src/transforms/RenameObjectFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { GraphQLNamedType, GraphQLField, GraphQLSchema } from 'graphql';
import { Transform } from './transforms';
import { createResolveType, fieldToFieldConfig } from '../stitching/schemaRecreation';
import { Request } from '../Interfaces';
import TransformObjectFields from './TransformObjectFields';

export default class RenameObjectFields implements Transform {
private transformer: TransformObjectFields;

constructor(renamer: (typeName: string, fieldName: string, field: GraphQLField<any, any>) => string) {
const resolveType = createResolveType((name: string, type: GraphQLNamedType): GraphQLNamedType => type);
this.transformer = new TransformObjectFields(
(typeName: string, fieldName: string, field: GraphQLField<any, any>) => {
return {
name: renamer(typeName, fieldName, field),
field: fieldToFieldConfig(field, resolveType, true)
};
}
);
}

public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema {
return this.transformer.transformSchema(originalSchema);
}

public transformRequest(originalRequest: Request): Request {
return this.transformer.transformRequest(originalRequest);
}
}
Loading

0 comments on commit 8f569e1

Please sign in to comment.