Skip to content

Commit

Permalink
v7 enhance(utils) expand filterSchema (#2005)
Browse files Browse the repository at this point in the history
* updated filter schema.

* update existing usage.
  • Loading branch information
gmac committed Sep 21, 2020
1 parent 9cbe464 commit bb7bd64
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 28 deletions.
2 changes: 1 addition & 1 deletion packages/stitch/tests/alternateStitchSchemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ describe('filter and rename object fields', () => {
]),
rootFieldFilter: (operation: string, fieldName: string) =>
`${operation}.${fieldName}` === 'Query.propertyById',
fieldFilter: (typeName: string, fieldName: string) =>
objectFieldFilter: (typeName: string, fieldName: string) =>
typeName === 'New_Property' || fieldName === 'name',
typeFilter: (typeName: string, type) =>
typeName === 'New_Property' ||
Expand Down
9 changes: 8 additions & 1 deletion packages/utils/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export type InputFieldFilter = (
export type FieldFilter = (
typeName?: string,
fieldName?: string,
fieldConfig?: GraphQLFieldConfig<any, any>
fieldConfig?: GraphQLFieldConfig<any, any> | GraphQLInputFieldConfig
) => boolean;

export type RootFieldFilter = (
Expand All @@ -204,6 +204,13 @@ export type RootFieldFilter = (

export type TypeFilter = (typeName: string, type: GraphQLType) => boolean;

export type ArgumentFilter = (
typeName?: string,
fieldName?: string,
argName?: string,
argConfig?: GraphQLArgumentConfig
) => boolean;

export type RenameTypesOptions = {
renameBuiltins: boolean;
renameScalars: boolean;
Expand Down
87 changes: 61 additions & 26 deletions packages/utils/src/filterSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,62 @@ import {
GraphQLSchema,
} from 'graphql';

import { MapperKind, FieldFilter, RootFieldFilter, TypeFilter } from './Interfaces';
import { MapperKind, FieldFilter, RootFieldFilter, TypeFilter, ArgumentFilter } from './Interfaces';

import { mapSchema } from './mapSchema';

import { Constructor } from './types';

export function filterSchema({
schema,
rootFieldFilter = () => true,
typeFilter = () => true,
fieldFilter = () => true,
objectFieldFilter = () => true,
interfaceFieldFilter = () => true,
fieldFilter = undefined,
rootFieldFilter = undefined,
objectFieldFilter = undefined,
interfaceFieldFilter = undefined,
inputObjectFieldFilter = undefined,
argumentFilter = undefined,
}: {
schema: GraphQLSchema;
rootFieldFilter?: RootFieldFilter;
typeFilter?: TypeFilter;
fieldFilter?: FieldFilter;
objectFieldFilter?: FieldFilter;
interfaceFieldFilter?: FieldFilter;
inputObjectFieldFilter?: FieldFilter;
argumentFilter?: ArgumentFilter;
}): GraphQLSchema {
const filteredSchema: GraphQLSchema = mapSchema(schema, {
[MapperKind.QUERY]: (type: GraphQLObjectType) => filterRootFields(type, 'Query', rootFieldFilter),
[MapperKind.MUTATION]: (type: GraphQLObjectType) => filterRootFields(type, 'Mutation', rootFieldFilter),
[MapperKind.SUBSCRIPTION]: (type: GraphQLObjectType) => filterRootFields(type, 'Subscription', rootFieldFilter),
[MapperKind.OBJECT_TYPE]: (type: GraphQLObjectType) =>
typeFilter(type.name, type)
? filterElementFields<GraphQLObjectType>(type, objectFieldFilter || fieldFilter, GraphQLObjectType)
? filterElementFields<GraphQLObjectType>(
GraphQLObjectType,
type,
objectFieldFilter || fieldFilter,
argumentFilter
)
: null,
[MapperKind.INTERFACE_TYPE]: (type: GraphQLInterfaceType) =>
typeFilter(type.name, type)
? filterElementFields<GraphQLInterfaceType>(type, interfaceFieldFilter, GraphQLInterfaceType)
? filterElementFields<GraphQLInterfaceType>(
GraphQLInterfaceType,
type,
interfaceFieldFilter || fieldFilter,
argumentFilter
)
: null,
[MapperKind.INPUT_OBJECT_TYPE]: (type: GraphQLInputObjectType) =>
typeFilter(type.name, type)
? filterElementFields<GraphQLInputObjectType>(
GraphQLInputObjectType,
type,
inputObjectFieldFilter || fieldFilter
)
: null,
[MapperKind.UNION_TYPE]: (type: GraphQLUnionType) => (typeFilter(type.name, type) ? undefined : null),
[MapperKind.INPUT_OBJECT_TYPE]: (type: GraphQLInputObjectType) => (typeFilter(type.name, type) ? undefined : null),
[MapperKind.ENUM_TYPE]: (type: GraphQLEnumType) => (typeFilter(type.name, type) ? undefined : null),
[MapperKind.SCALAR_TYPE]: (type: GraphQLScalarType) => (typeFilter(type.name, type) ? undefined : null),
});
Expand All @@ -53,27 +74,41 @@ export function filterSchema({
function filterRootFields(
type: GraphQLObjectType,
operation: 'Query' | 'Mutation' | 'Subscription',
rootFieldFilter: RootFieldFilter
rootFieldFilter?: RootFieldFilter
): GraphQLObjectType {
const config = type.toConfig();
Object.keys(config.fields).forEach(fieldName => {
if (!rootFieldFilter(operation, fieldName, config.fields[fieldName])) {
delete config.fields[fieldName];
}
});
return new GraphQLObjectType(config);
if (rootFieldFilter) {
const config = type.toConfig();
Object.keys(config.fields).forEach(fieldName => {
if (!rootFieldFilter(operation, fieldName, config.fields[fieldName])) {
delete config.fields[fieldName];
}
});
return new GraphQLObjectType(config);
}
return type;
}

function filterElementFields<ElementType>(
type: GraphQLObjectType | GraphQLInterfaceType,
fieldFilter: FieldFilter,
ElementConstructor: Constructor<ElementType>
): ElementType {
const config = type.toConfig();
Object.keys(config.fields).forEach(fieldName => {
if (!fieldFilter(type.name, fieldName, config.fields[fieldName])) {
delete config.fields[fieldName];
ElementConstructor: Constructor<ElementType>,
type: GraphQLObjectType | GraphQLInterfaceType | GraphQLInputObjectType,
fieldFilter?: FieldFilter,
argumentFilter?: ArgumentFilter
): ElementType | undefined {
if (fieldFilter || argumentFilter) {
if (!fieldFilter) fieldFilter = () => true;

const config = type.toConfig();
for (const [fieldName, field] of Object.entries(config.fields)) {
if (!fieldFilter(type.name, fieldName, config.fields[fieldName])) {
delete config.fields[fieldName];
} else if (argumentFilter && 'args' in field) {
for (const argName of Object.keys(field.args)) {
if (!argumentFilter(type.name, fieldName, argName, field.args[argName])) {
delete field.args[argName];
}
}
}
}
});
return new ElementConstructor(config);
return new ElementConstructor(config);
}
}
204 changes: 204 additions & 0 deletions packages/utils/tests/filterSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { makeExecutableSchema } from '@graphql-tools/schema';
import { filterSchema } from '@graphql-tools/utils';

describe('filterSchema', () => {
it('filters root fields', () => {
const schema = makeExecutableSchema({
typeDefs: `
type Query {
keep: String
omit: String
}
type Mutation {
keepThis(id: ID): String
omitThis(id: ID): String
}
`
});

const filtered = filterSchema({
schema,
rootFieldFilter: (opName, fieldName) => fieldName.startsWith('keep'),
});

expect(filtered.getType('Query').getFields()['keep']).toBeDefined();
expect(filtered.getType('Query').getFields()['omit']).toBeUndefined();
expect(filtered.getType('Mutation').getFields()['keepThis']).toBeDefined();
expect(filtered.getType('Mutation').getFields()['omitThis']).toBeUndefined();
});

it('filters types', () => {
const schema = makeExecutableSchema({
typeDefs: `
type Keep implements IKeep {
field(input: KeepInput): String
}
interface IKeep {
field(input: KeepInput): String
}
type Remove implements IRemove {
field(input: RemoveInput): String
}
interface IRemove {
field(input: RemoveInput): String
}
union KeepMany = Keep | Remove
union RemoveMany = Keep | Remove
input KeepInput {
field: String
}
input RemoveInput {
field: String
}
enum KeepValues {
VALUE
}
enum RemoveValues {
VALUE
}
scalar KeepScalar
scalar RemoveScalar
`
});

const filtered = filterSchema({
schema,
typeFilter: (typeName) => !/^I?Remove/.test(typeName)
});

expect(filtered.getType('Keep')).toBeDefined();
expect(filtered.getType('IKeep')).toBeDefined();
expect(filtered.getType('KeepMany')).toBeDefined();
expect(filtered.getType('KeepInput')).toBeDefined();
expect(filtered.getType('KeepValues')).toBeDefined();
expect(filtered.getType('KeepScalar')).toBeDefined();

expect(filtered.getType('Remove')).toBeUndefined();
expect(filtered.getType('IRemove')).toBeUndefined();
expect(filtered.getType('RemoveMany')).toBeUndefined();
expect(filtered.getType('RemoveInput')).toBeUndefined();
expect(filtered.getType('RemoveValues')).toBeUndefined();
expect(filtered.getType('RemoveScalar')).toBeUndefined();
});

it('filters object fields', () => {
const schema = makeExecutableSchema({
typeDefs: `
type Thing implements IThing {
keep: String
omit: String
}
interface IThing {
control: String
}
`
});

const filtered = filterSchema({
schema,
objectFieldFilter: (typeName, fieldName) => fieldName.startsWith('keep'),
});

expect(filtered.getType('Thing').getFields()['keep']).toBeDefined();
expect(filtered.getType('Thing').getFields()['omit']).toBeUndefined();
expect(filtered.getType('IThing').getFields()['control']).toBeDefined();
});

it('filters interface fields', () => {
const schema = makeExecutableSchema({
typeDefs: `
interface IThing {
keep: String
omit: String
}
type Thing implements IThing {
control: String
}
`
});

const filtered = filterSchema({
schema,
interfaceFieldFilter: (typeName, fieldName) => fieldName.startsWith('keep'),
});

expect(filtered.getType('IThing').getFields()['keep']).toBeDefined();
expect(filtered.getType('IThing').getFields()['omit']).toBeUndefined();
expect(filtered.getType('Thing').getFields()['control']).toBeDefined();
});

it('filters input object fields', () => {
const schema = makeExecutableSchema({
typeDefs: `
input ThingInput {
keep: String
omit: String
}
type Thing {
control: String
}
`
});

const filtered = filterSchema({
schema,
inputObjectFieldFilter: (typeName, fieldName) => fieldName.startsWith('keep'),
});

expect(filtered.getType('ThingInput').getFields()['keep']).toBeDefined();
expect(filtered.getType('ThingInput').getFields()['omit']).toBeUndefined();
expect(filtered.getType('Thing').getFields()['control']).toBeDefined();
});

it('filters all field types', () => {
const schema = makeExecutableSchema({
typeDefs: `
type Thing implements IThing {
keep: String
omit: String
}
interface IThing {
keep: String
omit: String
}
input ThingInput {
keep: String
omit: String
}
`
});

const filtered = filterSchema({
schema,
fieldFilter: (typeName, fieldName) => fieldName.startsWith('keep'),
});

expect(filtered.getType('Thing').getFields()['keep']).toBeDefined();
expect(filtered.getType('Thing').getFields()['omit']).toBeUndefined();
expect(filtered.getType('IThing').getFields()['keep']).toBeDefined();
expect(filtered.getType('IThing').getFields()['omit']).toBeUndefined();
expect(filtered.getType('ThingInput').getFields()['keep']).toBeDefined();
expect(filtered.getType('ThingInput').getFields()['omit']).toBeUndefined();
});

it('filters all arguments', () => {
const schema = makeExecutableSchema({
typeDefs: `
type Thing implements IThing {
field(keep: String, omit: String): String
}
interface IThing {
field(keep: String, omit: String): String
}
`
});

const filtered = filterSchema({
schema,
argumentFilter: (typeName, fieldName, argName) => argName.startsWith('keep'),
});

expect(filtered.getType('Thing').getFields()['field'].args.map(arg => arg.name)).toEqual(['keep']);
expect(filtered.getType('IThing').getFields()['field'].args.map(arg => arg.name)).toEqual(['keep']);
});
});

0 comments on commit bb7bd64

Please sign in to comment.