Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transforms for normalizing stitched subschema deprecations #1925

Merged
merged 30 commits into from
Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
28885c6
[deploy_website] add transforms for normalizing subschema deprecations.
gmac Aug 19, 2020
23f2468
fix import/export
gmac Aug 19, 2020
a3b6ee5
fix linting errors.
gmac Aug 19, 2020
3268533
[deploy_website] my crash course in typescript.
gmac Aug 19, 2020
0eb33a7
eat more dogfood.
gmac Aug 19, 2020
a543d19
im terrible at typescript.
gmac Aug 19, 2020
713154b
refactor into respective libs.
gmac Aug 19, 2020
ea3778e
yeah, whoops.
gmac Aug 19, 2020
bceead6
is this right?
gmac Aug 19, 2020
e7c6358
also check the ast composition.
gmac Aug 19, 2020
48ed54c
fix rogue typing.
gmac Aug 19, 2020
3d405e0
shrink diff.
gmac Aug 19, 2020
415773c
rename RemoveFieldsWithDirective.
gmac Aug 19, 2020
67e931d
another better name.
gmac Aug 19, 2020
894478e
Fix documentation mistake
gmac Aug 20, 2020
88f9cab
fix incomplete test.
gmac Aug 20, 2020
1f7e249
compare typed directives.
gmac Aug 21, 2020
d3dd95e
add tests for empty criteria.
gmac Aug 21, 2020
d47ef15
refactor
yaacovCR Aug 21, 2020
dbd92f5
small fix
yaacovCR Aug 21, 2020
c129730
Merge pull request #1 from ardatan/hmmmm
gmac Aug 22, 2020
f3306e9
Merge branch 'master' of https://github.com/ardatan/graphql-tools int…
gmac Aug 22, 2020
52480d7
add FilterFieldDirectives tests.
gmac Aug 22, 2020
4c366a2
more tests for valueMatchesCriteria.
gmac Aug 22, 2020
2ff6fa9
cleanup stitching package tests.
gmac Aug 22, 2020
03564a4
break apart deprecations from directive filters.
gmac Aug 23, 2020
4d2b3d9
cleanup naming and fix typescript error.
gmac Aug 23, 2020
57b7d67
[deploy_website] remove nested ifs.
gmac Aug 23, 2020
ce730da
eat more dogfood.
gmac Aug 23, 2020
eeafdad
renaming.
gmac Aug 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ export * from './errors';
export * from './toConfig';
export * from './observableToAsyncIterable';
export * from './visitResult';
export * from './getArgumentValues';
export * from './valueMatchesCriteria';
17 changes: 17 additions & 0 deletions packages/utils/src/valueMatchesCriteria.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export function valueMatchesCriteria(value: any, criteria: any): boolean {
if (value == null) {
return value === criteria;
} else if (Array.isArray(value)) {
return Array.isArray(criteria) && value.every((val, index) => valueMatchesCriteria(val, criteria[index]));
} else if (typeof value === 'object') {
return (
typeof criteria === 'object' &&
criteria &&
Object.keys(criteria).every(propertyName => valueMatchesCriteria(value[propertyName], criteria[propertyName]))
);
} else if (criteria instanceof RegExp) {
return criteria.test(value);
}

return value === criteria;
}
59 changes: 59 additions & 0 deletions packages/utils/tests/valueMatchesCriteria.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { valueMatchesCriteria } from '../src/index';

describe('valueMatchesCriteria', () => {
test('matches empty values', () => {
expect(valueMatchesCriteria(undefined, undefined)).toBe(true);
expect(valueMatchesCriteria(undefined, null)).toBe(false);
expect(valueMatchesCriteria(null, null)).toBe(true);
});

test('matches primitives', () => {
expect(valueMatchesCriteria(1, 1)).toBe(true);
expect(valueMatchesCriteria(1, 2)).toBe(false);
expect(valueMatchesCriteria('a', 'a')).toBe(true);
expect(valueMatchesCriteria('a', 'b')).toBe(false);
expect(valueMatchesCriteria(false, false)).toBe(true);
expect(valueMatchesCriteria(false, true)).toBe(false);
});

test('matches empty object values', () => {
expect(valueMatchesCriteria({}, {})).toBe(true);
});

test('matches value object with varying specificity', () => {
const dirValue = { reason: 'reason', also: 'also' };

expect(valueMatchesCriteria(dirValue, {})).toBe(true);
expect(valueMatchesCriteria(dirValue, { reason: 'reason' })).toBe(true);
expect(valueMatchesCriteria(dirValue, { reason: 'reason', also: 'also' })).toBe(true);
expect(valueMatchesCriteria(dirValue, { reason: 'reason', and: 'and' })).toBe(false);
expect(valueMatchesCriteria(dirValue, { this: 'this' })).toBe(false);
expect(valueMatchesCriteria(dirValue, { reason: 'this' })).toBe(false);
});

test('matches value objects recursively', () => {
const dirValue = { reason: 'reason', also: { a: 1, b: 2 } };

expect(valueMatchesCriteria(dirValue, { reason: 'reason' })).toBe(true);
expect(valueMatchesCriteria(dirValue, { also: {} })).toBe(true);
expect(valueMatchesCriteria(dirValue, { also: { a: 1 } })).toBe(true);
expect(valueMatchesCriteria(dirValue, { also: { a: 1, b: 2 } })).toBe(true);
expect(valueMatchesCriteria(dirValue, { also: { a: 1, b: 0 } })).toBe(false);
expect(valueMatchesCriteria(dirValue, { also: { c: 1 } })).toBe(false);
});

test('matches value arrays', () => {
const dirValue = [23, { hello: true, world: false }];

expect(valueMatchesCriteria(dirValue, [23, { hello: true }])).toBe(true);
expect(valueMatchesCriteria(dirValue, [23, { world: false }])).toBe(true);
expect(valueMatchesCriteria(dirValue, [{ hello: true }, 23])).toBe(false);
});

test('matches value with regex', () => {
const dirValue = { reason: 'requires: id' };

expect(valueMatchesCriteria(dirValue, { reason: /^requires:/ })).toBe(true);
expect(valueMatchesCriteria(dirValue, { reason: /^required:/ })).toBe(false);
});
});
36 changes: 36 additions & 0 deletions packages/wrap/src/transforms/FilterObjectFieldDirectives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { GraphQLSchema, GraphQLFieldConfig } from 'graphql';
import { Transform, getArgumentValues } from '@graphql-tools/utils';
import TransformObjectFields from './TransformObjectFields';

export default class FilterObjectFieldDirectives implements Transform {
private readonly filter: (dirName: string, dirValue: any) => boolean;

constructor(filter: (dirName: string, dirValue: any) => boolean) {
this.filter = filter;
}

public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema {
const transformer = new TransformObjectFields(
(_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig<any, any>) => {
const keepDirectives = fieldConfig.astNode.directives.filter(dir => {
const directiveDef = originalSchema.getDirective(dir.name.value);
const directiveValue = directiveDef ? getArgumentValues(directiveDef, dir) : undefined;
return this.filter(dir.name.value, directiveValue);
});

if (keepDirectives.length !== fieldConfig.astNode.directives.length) {
fieldConfig = {
...fieldConfig,
astNode: {
...fieldConfig.astNode,
directives: keepDirectives,
},
};
return fieldConfig;
}
}
);

return transformer.transformSchema(originalSchema);
}
}
28 changes: 28 additions & 0 deletions packages/wrap/src/transforms/RemoveObjectFieldDeprecations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { GraphQLSchema, GraphQLFieldConfig } from 'graphql';
import { Transform, valueMatchesCriteria } from '@graphql-tools/utils';
import { FilterObjectFieldDirectives, TransformObjectFields } from '@graphql-tools/wrap';

export default class RemoveObjectFieldDeprecations implements Transform {
private readonly removeDirectives: FilterObjectFieldDirectives;
private readonly removeDeprecations: TransformObjectFields;

constructor(reason: string | RegExp) {
const args = { reason };
this.removeDirectives = new FilterObjectFieldDirectives((dirName: string, dirValue: any) => {
return !(dirName === 'deprecated' && valueMatchesCriteria(dirValue, args));
});
this.removeDeprecations = new TransformObjectFields(
(_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig<any, any>) => {
if (fieldConfig.deprecationReason && valueMatchesCriteria(fieldConfig.deprecationReason, reason)) {
fieldConfig = { ...fieldConfig };
delete fieldConfig.deprecationReason;
}
return fieldConfig;
}
);
}

public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema {
return this.removeDeprecations.transformSchema(this.removeDirectives.transformSchema(originalSchema));
}
}
17 changes: 17 additions & 0 deletions packages/wrap/src/transforms/RemoveObjectFieldDirectives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GraphQLSchema } from 'graphql';
import { Transform, valueMatchesCriteria } from '@graphql-tools/utils';
import { FilterObjectFieldDirectives } from '@graphql-tools/wrap';

export default class RemoveObjectFieldDirectives implements Transform {
private readonly transformer: FilterObjectFieldDirectives;

constructor(directiveName: string | RegExp, args: Record<string, any> = {}) {
this.transformer = new FilterObjectFieldDirectives((dirName: string, dirValue: any) => {
return !(valueMatchesCriteria(dirName, directiveName) && valueMatchesCriteria(dirValue, args));
});
}

public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema {
return this.transformer.transformSchema(originalSchema);
}
}
22 changes: 22 additions & 0 deletions packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { GraphQLSchema, GraphQLFieldConfig } from 'graphql';
import { Transform, valueMatchesCriteria } from '@graphql-tools/utils';
import { FilterObjectFields } from '@graphql-tools/wrap';

export default class RemoveObjectFieldsWithDeprecation implements Transform {
private readonly transformer: FilterObjectFields;

constructor(reason: string | RegExp) {
this.transformer = new FilterObjectFields(
(_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig<any, any>) => {
if (fieldConfig.deprecationReason) {
return !valueMatchesCriteria(fieldConfig.deprecationReason, reason);
}
return true;
}
);
}

public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema {
return this.transformer.transformSchema(originalSchema);
}
}
30 changes: 30 additions & 0 deletions packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { GraphQLSchema, GraphQLFieldConfig } from 'graphql';
import { Transform, getDirectives, valueMatchesCriteria } from '@graphql-tools/utils';
import { FilterObjectFields } from '@graphql-tools/wrap';

export default class RemoveObjectFieldsWithDirective implements Transform {
private readonly directiveName: string | RegExp;
private readonly args: Record<string, any>;

constructor(directiveName: string | RegExp, args: Record<string, any> = {}) {
this.directiveName = directiveName;
this.args = args;
}

public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema {
const transformer = new FilterObjectFields(
(_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig<any, any>) => {
const valueMap = getDirectives(originalSchema, fieldConfig);
return !Object.keys(valueMap).some(
directiveName =>
valueMatchesCriteria(directiveName, this.directiveName) &&
((Array.isArray(valueMap[directiveName]) &&
valueMap[directiveName].some((value: any) => valueMatchesCriteria(value, this.args))) ||
valueMatchesCriteria(valueMap[directiveName], this.args))
);
}
);

return transformer.transformSchema(originalSchema);
}
}
5 changes: 5 additions & 0 deletions packages/wrap/src/transforms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export { default as FilterInputObjectFields } from './FilterInputObjectFields';
export { default as MapLeafValues } from './MapLeafValues';
export { default as TransformEnumValues } from './TransformEnumValues';
export { default as TransformQuery } from './TransformQuery';
export { default as FilterObjectFieldDirectives } from './FilterObjectFieldDirectives';
export { default as RemoveObjectFieldDirectives } from './RemoveObjectFieldDirectives';
export { default as RemoveObjectFieldsWithDirective } from './RemoveObjectFieldsWithDirective';
export { default as RemoveObjectFieldDeprecations } from './RemoveObjectFieldDeprecations';
export { default as RemoveObjectFieldsWithDeprecation } from './RemoveObjectFieldsWithDeprecation';

export { default as ExtendSchema } from './ExtendSchema';
export { default as PruneSchema } from './PruneSchema';
Expand Down
29 changes: 29 additions & 0 deletions packages/wrap/tests/transformFilterObjectFieldDirectives.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { wrapSchema, FilterObjectFieldDirectives } from '@graphql-tools/wrap';
import { makeExecutableSchema } from '@graphql-tools/schema';

describe('FilterObjectFieldDirectives', () => {
test('removes unmatched field directives', async () => {
const schema = makeExecutableSchema({
typeDefs: `
directive @remove on FIELD_DEFINITION
directive @keep(arg: Int) on FIELD_DEFINITION
type Query {
alpha:String @remove
bravo:String @keep
charlie:String @keep(arg:1)
delta:String @keep(arg:2)
}
`
});

const transformedSchema = wrapSchema(schema, [
new FilterObjectFieldDirectives((dirName: string, dirValue: any) => dirName === 'keep' && dirValue.arg !== 1)
]);

const fields = transformedSchema.getType('Query').getFields();
expect(fields.alpha.astNode.directives.length).toEqual(0);
expect(fields.bravo.astNode.directives.length).toEqual(1);
expect(fields.charlie.astNode.directives.length).toEqual(0);
expect(fields.delta.astNode.directives.length).toEqual(1);
});
});
38 changes: 38 additions & 0 deletions packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { wrapSchema, RemoveObjectFieldDeprecations } from '@graphql-tools/wrap';
import { makeExecutableSchema } from '@graphql-tools/schema';

describe('RemoveObjectFieldDeprecations', () => {
const originalSchema = makeExecutableSchema({
typeDefs: `
type Test {
id: ID!
first: String! @deprecated(reason: "do not remove")
second: String! @deprecated(reason: "remove this")
}
`
});

test('removes deprecations by reason', async () => {
const transformedSchema = wrapSchema(originalSchema, [
new RemoveObjectFieldDeprecations('remove this')
]);

const fields = transformedSchema.getType('Test').getFields();
expect(fields.first.deprecationReason).toEqual('do not remove');
expect(fields.second.deprecationReason).toBeUndefined();
expect(fields.first.astNode.directives.length).toEqual(1);
expect(fields.second.astNode.directives.length).toEqual(0);
});

test('removes deprecations by reason regex', async () => {
const transformedSchema = wrapSchema(originalSchema, [
new RemoveObjectFieldDeprecations(/remove/)
]);

const fields = transformedSchema.getType('Test').getFields();
expect(fields.first.deprecationReason).toBeUndefined();
expect(fields.second.deprecationReason).toBeUndefined();
expect(fields.first.astNode.directives.length).toEqual(0);
expect(fields.second.astNode.directives.length).toEqual(0);
});
});
66 changes: 66 additions & 0 deletions packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { wrapSchema, RemoveObjectFieldDirectives } from '@graphql-tools/wrap';
import { makeExecutableSchema } from '@graphql-tools/schema';

describe('RemoveObjectFieldDirectives', () => {
const originalSchema = makeExecutableSchema({
typeDefs: `
directive @alpha(arg: String) on FIELD_DEFINITION
directive @bravo(arg: String) on FIELD_DEFINITION

type Test {
id: ID! @bravo(arg: "remove this")
first: String! @alpha(arg: "do not remove")
second: String! @alpha(arg: "remove this")
third: String @alpha(arg: "remove this")
}
`
});

test('removes directives by name', async () => {
const transformedSchema = wrapSchema(originalSchema, [
new RemoveObjectFieldDirectives('alpha')
]);

const fields = transformedSchema.getType('Test').getFields();
expect(fields.id.astNode.directives.length).toEqual(1);
expect(fields.first.astNode.directives.length).toEqual(0);
expect(fields.second.astNode.directives.length).toEqual(0);
expect(fields.third.astNode.directives.length).toEqual(0);
});

test('removes directives by name regex', async () => {
const transformedSchema = wrapSchema(originalSchema, [
new RemoveObjectFieldDirectives(/^alp/)
]);

const fields = transformedSchema.getType('Test').getFields();
expect(fields.id.astNode.directives.length).toEqual(1);
expect(fields.first.astNode.directives.length).toEqual(0);
expect(fields.second.astNode.directives.length).toEqual(0);
expect(fields.third.astNode.directives.length).toEqual(0);
});

test('removes directives by argument', async () => {
const transformedSchema = wrapSchema(originalSchema, [
new RemoveObjectFieldDirectives(/.+/, { arg: 'remove this' })
]);

const fields = transformedSchema.getType('Test').getFields();
expect(fields.id.astNode.directives.length).toEqual(0);
expect(fields.first.astNode.directives.length).toEqual(1);
expect(fields.second.astNode.directives.length).toEqual(0);
expect(fields.third.astNode.directives.length).toEqual(0);
});

test('removes directives by argument regex', async () => {
const transformedSchema = wrapSchema(originalSchema, [
new RemoveObjectFieldDirectives(/.+/, { arg: /remove/ })
]);

const fields = transformedSchema.getType('Test').getFields();
expect(fields.id.astNode.directives.length).toEqual(0);
expect(fields.first.astNode.directives.length).toEqual(0);
expect(fields.second.astNode.directives.length).toEqual(0);
expect(fields.third.astNode.directives.length).toEqual(0);
});
});