diff --git a/packages/stitch/tests/alternateStitchSchemas.test.ts b/packages/stitch/tests/alternateStitchSchemas.test.ts index 93717f9307c..c07b8863c38 100644 --- a/packages/stitch/tests/alternateStitchSchemas.test.ts +++ b/packages/stitch/tests/alternateStitchSchemas.test.ts @@ -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' || diff --git a/packages/utils/src/Interfaces.ts b/packages/utils/src/Interfaces.ts index 641894c0410..a207dd558d5 100644 --- a/packages/utils/src/Interfaces.ts +++ b/packages/utils/src/Interfaces.ts @@ -193,7 +193,7 @@ export type InputFieldFilter = ( export type FieldFilter = ( typeName?: string, fieldName?: string, - fieldConfig?: GraphQLFieldConfig + fieldConfig?: GraphQLFieldConfig | GraphQLInputFieldConfig ) => boolean; export type RootFieldFilter = ( @@ -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; diff --git a/packages/utils/src/filterSchema.ts b/packages/utils/src/filterSchema.ts index c1a52895c9c..934bbb9537f 100644 --- a/packages/utils/src/filterSchema.ts +++ b/packages/utils/src/filterSchema.ts @@ -8,7 +8,7 @@ import { GraphQLSchema, } from 'graphql'; -import { MapperKind, FieldFilter, RootFieldFilter, TypeFilter } from './Interfaces'; +import { MapperKind, FieldFilter, RootFieldFilter, TypeFilter, ArgumentFilter } from './Interfaces'; import { mapSchema } from './mapSchema'; @@ -16,11 +16,13 @@ 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; @@ -28,6 +30,8 @@ export function filterSchema({ fieldFilter?: FieldFilter; objectFieldFilter?: FieldFilter; interfaceFieldFilter?: FieldFilter; + inputObjectFieldFilter?: FieldFilter; + argumentFilter?: ArgumentFilter; }): GraphQLSchema { const filteredSchema: GraphQLSchema = mapSchema(schema, { [MapperKind.QUERY]: (type: GraphQLObjectType) => filterRootFields(type, 'Query', rootFieldFilter), @@ -35,14 +39,31 @@ export function filterSchema({ [MapperKind.SUBSCRIPTION]: (type: GraphQLObjectType) => filterRootFields(type, 'Subscription', rootFieldFilter), [MapperKind.OBJECT_TYPE]: (type: GraphQLObjectType) => typeFilter(type.name, type) - ? filterElementFields(type, objectFieldFilter || fieldFilter, GraphQLObjectType) + ? filterElementFields( + GraphQLObjectType, + type, + objectFieldFilter || fieldFilter, + argumentFilter + ) : null, [MapperKind.INTERFACE_TYPE]: (type: GraphQLInterfaceType) => typeFilter(type.name, type) - ? filterElementFields(type, interfaceFieldFilter, GraphQLInterfaceType) + ? filterElementFields( + GraphQLInterfaceType, + type, + interfaceFieldFilter || fieldFilter, + argumentFilter + ) + : null, + [MapperKind.INPUT_OBJECT_TYPE]: (type: GraphQLInputObjectType) => + typeFilter(type.name, type) + ? filterElementFields( + 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), }); @@ -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( - type: GraphQLObjectType | GraphQLInterfaceType, - fieldFilter: FieldFilter, - ElementConstructor: Constructor -): 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, + 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); + } } diff --git a/packages/utils/tests/filterSchema.test.ts b/packages/utils/tests/filterSchema.test.ts new file mode 100644 index 00000000000..0c1bd3ec3e6 --- /dev/null +++ b/packages/utils/tests/filterSchema.test.ts @@ -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']); + }); +});