From 28885c680f8c33d569ba5869b07a8c209e75aae9 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 09:58:31 -0400 Subject: [PATCH 01/28] [deploy_website] add transforms for normalizing subschema deprecations. --- packages/stitch/src/index.ts | 2 + .../src/transforms/RemoveDeprecatedFields.ts | 13 +++++ .../src/transforms/RemoveDeprecations.ts | 25 ++++++++ .../tests/transformDeprecations.test.ts | 57 +++++++++++++++++++ website/docs/stitch-type-merging.md | 53 +++++++++++++++-- 5 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 packages/stitch/src/transforms/RemoveDeprecatedFields.ts create mode 100644 packages/stitch/src/transforms/RemoveDeprecations.ts create mode 100644 packages/stitch/tests/transformDeprecations.test.ts diff --git a/packages/stitch/src/index.ts b/packages/stitch/src/index.ts index 5aa711ccfb4..c5a95241f50 100755 --- a/packages/stitch/src/index.ts +++ b/packages/stitch/src/index.ts @@ -1,2 +1,4 @@ export { stitchSchemas } from './stitchSchemas'; export { forwardArgsToSelectionSet } from './selectionSetArgs'; +export { RemoveDeprecations } from './transforms/RemoveDeprecations'; +export { RemoveDeprecatedFields } from './transforms/RemoveDeprecatedFields'; diff --git a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts b/packages/stitch/src/transforms/RemoveDeprecatedFields.ts new file mode 100644 index 00000000000..125b9cbd374 --- /dev/null +++ b/packages/stitch/src/transforms/RemoveDeprecatedFields.ts @@ -0,0 +1,13 @@ +import { Transform, MapperKind, mapSchema } from '@graphql-tools/utils'; + +export default class RemoveDeprecatedFields implements Transform { + constructor(reason) { + this.reason = reason; + } + + transformSchema(schema) { + return mapSchema(schema, { + [MapperKind.FIELD]: field => (field.deprecationReason === this.reason ? undefined : null), + }); + } +} diff --git a/packages/stitch/src/transforms/RemoveDeprecations.ts b/packages/stitch/src/transforms/RemoveDeprecations.ts new file mode 100644 index 00000000000..80aa2bc0108 --- /dev/null +++ b/packages/stitch/src/transforms/RemoveDeprecations.ts @@ -0,0 +1,25 @@ +import { Transform, MapperKind, mapSchema } from '@graphql-tools/utils'; + +export default class RemoveDeprecations implements Transform { + constructor(reason) { + this.reason = reason; + } + + transformSchema(schema) { + return mapSchema(schema, { + [MapperKind.FIELD]: field => { + if (field.deprecationReason === this.reason) { + field = { + ...field, + astNode: { + ...field.astNode, + directives: field.astNode.directives.filter(dir => dir.name.value !== 'deprecated'), + }, + }; + delete field.deprecationReason; + return field; + } + }, + }); + } +} diff --git a/packages/stitch/tests/transformDeprecations.test.ts b/packages/stitch/tests/transformDeprecations.test.ts new file mode 100644 index 00000000000..ef2135e83fa --- /dev/null +++ b/packages/stitch/tests/transformDeprecations.test.ts @@ -0,0 +1,57 @@ +import { graphql } from 'graphql'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { stitchSchemas } from '../src/stitchSchemas'; +import RemoveDeprecations from '../src/transforms/RemoveDeprecations'; +import RemoveDeprecatedFields from '../src/transforms/RemoveDeprecatedFields'; + +describe('transform deprecations', () => { + test('removes specific deprecations and deprecated fields from the gateway schema', async () => { + const listingsSchema = makeExecutableSchema({ + typeDefs: ` + type Listing { + id: ID! + description: String! + price: Float! + sellerId: ID! @deprecated(reason: "stitching use only") + buyerId: ID @deprecated(reason: "stitching use only") + } + ` + }); + + const usersSchema = makeExecutableSchema({ + typeDefs: ` + type User { + id: ID! + email: String! @deprecated(reason: "other deprecation") + } + + type Listing { + seller: User! @deprecated(reason: "gateway access only") + buyer: User @deprecated(reason: "gateway access only") + } + ` + }); + + const gatewaySchema = stitchSchemas({ + subschemas: [ + { + schema: listingsSchema, + transforms: [new RemoveDeprecatedFields('stitching use only')] + }, + { + schema: usersSchema, + transforms: [new RemoveDeprecations('gateway access only')] + }, + ], + }); + + expect(listingsSchema.getType('Listing').getFields().sellerId.deprecationReason).toBe('stitching use only'); + expect(gatewaySchema.getType('Listing').getFields().sellerId).toBe(undefined); + + expect(usersSchema.getType('Listing').getFields().seller.deprecationReason).toBe('gateway access only'); + expect(gatewaySchema.getType('Listing').getFields().seller.deprecationReason).toBe(undefined); + + expect(usersSchema.getType('User').getFields().email.deprecationReason).toBe('other deprecation'); + expect(gatewaySchema.getType('User').getFields().email.deprecationReason).toBe('other deprecation'); + }); +}); diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index 3edac4a796f..6840022aa6e 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -341,7 +341,7 @@ Some important features to notice in the above schema: - Users service `Listing` now _only_ provides `buyer` and `seller` associations without any need for a shared `id`. - Users service defines a `ListingRepresentation` input for external keys, and a `_listingsByReps` query that recieves them. -To bring this all together, the gateway orchestrates collecting plain keys from the listing service, and then injecting them as representations of external records into the users service... from which they return as a complete type: +To bring this all together, the gateway orchestrates collecting plain keys from the listing service, and then injects them as representations of external records into the users service... ```js const gatewaySchema = stitchSchemas({ @@ -373,9 +373,9 @@ const gatewaySchema = stitchSchemas({ }); ``` -In summary, the gateway had selected `buyerId` and `sellerId` fields from the listings services, sent those keys as input over to the users service, and then recieved back a complete type resolved with multiple fields of any type and selection. Neat! +To recap, the gateway has selected `buyerId` and `sellerId` fields from the listings services, sent those keys as input over to the users service, and then recieved back a complete type resolved with multiple fields of any type and selection. Neat! -However, you may notice that both `sellerId` and `buyerId` keys are _always_ requested from the listing service, even though they are only needed when resolving their respective associations. If we were sensitive to costs associated with keys, then we could judiciously select only the keys needed for the query with a field-level selectionSet mapping: +However, you may notice that both `buyerId` and `sellerId` keys are _always_ requested from the listing service, even though they are only needed when resolving their respective associations. If we were sensitive to costs associated with keys, then we could judiciously select only the keys needed for the query with a field-level selectionSet mapping: ```js { @@ -394,7 +394,52 @@ However, you may notice that both `sellerId` and `buyerId` keys are _always_ req } ``` -One minor disadvantage of this pattern is that the listings service includes ugly `sellerId` and `buyerId` fields. There's no harm in marking these IDs as `@deprecated`, or they may be removed completely from the gateway schema using a [transform](/docs/stitch-combining-schemas#adding-transforms). +One disadvantage of this pattern is that we end up with clutter—`buyerId`/`sellerId` are extra fields, and `buyer`/`seller` fields have gateway dependencies. To tidy things up, we can aggressively deprecate these fields in subschemas and then remove/normalize their behavior in the gateway using available transforms: + +```js +import { RemoveDeprecatedFields, RemoveDeprecations } from '@graphql-tools/stitch'; + +const listingsSchema = makeExecutableSchema({ + typeDefs: ` + type Listing { + id: ID! + description: String! + price: Float! + sellerId: ID! @deprecated(reason: "stitching use only") + buyerId: ID @deprecated(reason: "stitching use only") + } + ` +}); + +const usersSchema = makeExecutableSchema({ + typeDefs: ` + type User { + id: ID! + email: String! + } + + type Listing { + seller: User! @deprecated(reason: "gateway access only") + buyer: User @deprecated(reason: "gateway access only") + } + ` +}); + +const gatewaySchema = stitchSchemas({ + subschemas: [ + { + schema: listingsSchema, + transforms: [new RemoveDeprecatedFields('stitching use only')], + merge: { ... } + }, + { + schema: usersSchema, + transforms: [new RemoveDeprecations('gateway access only')], + merge: { ... } + }, + ], +}); +``` ### Federation services From 23f2468203bcf13425d60b688c6337dcd5864983 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 10:08:51 -0400 Subject: [PATCH 02/28] fix import/export --- packages/stitch/src/transforms/RemoveDeprecatedFields.ts | 2 +- packages/stitch/src/transforms/RemoveDeprecations.ts | 2 +- packages/stitch/tests/transformDeprecations.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts b/packages/stitch/src/transforms/RemoveDeprecatedFields.ts index 125b9cbd374..18391b19d6e 100644 --- a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts +++ b/packages/stitch/src/transforms/RemoveDeprecatedFields.ts @@ -1,6 +1,6 @@ import { Transform, MapperKind, mapSchema } from '@graphql-tools/utils'; -export default class RemoveDeprecatedFields implements Transform { +export class RemoveDeprecatedFields implements Transform { constructor(reason) { this.reason = reason; } diff --git a/packages/stitch/src/transforms/RemoveDeprecations.ts b/packages/stitch/src/transforms/RemoveDeprecations.ts index 80aa2bc0108..aff52aeffc5 100644 --- a/packages/stitch/src/transforms/RemoveDeprecations.ts +++ b/packages/stitch/src/transforms/RemoveDeprecations.ts @@ -1,6 +1,6 @@ import { Transform, MapperKind, mapSchema } from '@graphql-tools/utils'; -export default class RemoveDeprecations implements Transform { +export class RemoveDeprecations implements Transform { constructor(reason) { this.reason = reason; } diff --git a/packages/stitch/tests/transformDeprecations.test.ts b/packages/stitch/tests/transformDeprecations.test.ts index ef2135e83fa..81172cbda43 100644 --- a/packages/stitch/tests/transformDeprecations.test.ts +++ b/packages/stitch/tests/transformDeprecations.test.ts @@ -1,8 +1,8 @@ import { graphql } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '../src/stitchSchemas'; -import RemoveDeprecations from '../src/transforms/RemoveDeprecations'; -import RemoveDeprecatedFields from '../src/transforms/RemoveDeprecatedFields'; +import { RemoveDeprecations } from '../src/index'; +import { RemoveDeprecatedFields } from '../src/index'; describe('transform deprecations', () => { test('removes specific deprecations and deprecated fields from the gateway schema', async () => { From a3b6ee59b12090365dad8f19df3866c5d33bb5f0 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 10:13:03 -0400 Subject: [PATCH 03/28] fix linting errors. --- packages/stitch/tests/transformDeprecations.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/stitch/tests/transformDeprecations.test.ts b/packages/stitch/tests/transformDeprecations.test.ts index 81172cbda43..2b674762566 100644 --- a/packages/stitch/tests/transformDeprecations.test.ts +++ b/packages/stitch/tests/transformDeprecations.test.ts @@ -1,8 +1,6 @@ -import { graphql } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '../src/stitchSchemas'; -import { RemoveDeprecations } from '../src/index'; -import { RemoveDeprecatedFields } from '../src/index'; +import { RemoveDeprecations, RemoveDeprecatedFields } from '../src/index'; describe('transform deprecations', () => { test('removes specific deprecations and deprecated fields from the gateway schema', async () => { From 3268533eb67b4195e53d62ae9c2232967367a1ee Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 10:22:35 -0400 Subject: [PATCH 04/28] [deploy_website] my crash course in typescript. --- packages/stitch/src/transforms/RemoveDeprecatedFields.ts | 7 +++++-- packages/stitch/src/transforms/RemoveDeprecations.ts | 7 +++++-- packages/stitch/tests/transformDeprecations.test.ts | 4 ++-- website/docs/stitch-type-merging.md | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts b/packages/stitch/src/transforms/RemoveDeprecatedFields.ts index 18391b19d6e..c752c914c71 100644 --- a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts +++ b/packages/stitch/src/transforms/RemoveDeprecatedFields.ts @@ -1,11 +1,14 @@ +import { GraphQLSchema } from 'graphql'; import { Transform, MapperKind, mapSchema } from '@graphql-tools/utils'; export class RemoveDeprecatedFields implements Transform { - constructor(reason) { + private readonly reason: string; + + constructor({ reason }: { reason: string }) { this.reason = reason; } - transformSchema(schema) { + public transformSchema(schema: GraphQLSchema): GraphQLSchema { return mapSchema(schema, { [MapperKind.FIELD]: field => (field.deprecationReason === this.reason ? undefined : null), }); diff --git a/packages/stitch/src/transforms/RemoveDeprecations.ts b/packages/stitch/src/transforms/RemoveDeprecations.ts index aff52aeffc5..ef2252f3409 100644 --- a/packages/stitch/src/transforms/RemoveDeprecations.ts +++ b/packages/stitch/src/transforms/RemoveDeprecations.ts @@ -1,11 +1,14 @@ +import { GraphQLSchema } from 'graphql'; import { Transform, MapperKind, mapSchema } from '@graphql-tools/utils'; export class RemoveDeprecations implements Transform { - constructor(reason) { + private readonly reason: string; + + constructor({ reason }: { reason: string }) { this.reason = reason; } - transformSchema(schema) { + public transformSchema(schema: GraphQLSchema): GraphQLSchema { return mapSchema(schema, { [MapperKind.FIELD]: field => { if (field.deprecationReason === this.reason) { diff --git a/packages/stitch/tests/transformDeprecations.test.ts b/packages/stitch/tests/transformDeprecations.test.ts index 2b674762566..3cd62696f56 100644 --- a/packages/stitch/tests/transformDeprecations.test.ts +++ b/packages/stitch/tests/transformDeprecations.test.ts @@ -34,11 +34,11 @@ describe('transform deprecations', () => { subschemas: [ { schema: listingsSchema, - transforms: [new RemoveDeprecatedFields('stitching use only')] + transforms: [new RemoveDeprecatedFields({ reason: 'stitching use only' })] }, { schema: usersSchema, - transforms: [new RemoveDeprecations('gateway access only')] + transforms: [new RemoveDeprecations({ reason: 'gateway access only' })] }, ], }); diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index 6840022aa6e..dd426cb04c1 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -429,12 +429,12 @@ const gatewaySchema = stitchSchemas({ subschemas: [ { schema: listingsSchema, - transforms: [new RemoveDeprecatedFields('stitching use only')], + transforms: [new RemoveDeprecatedFields({ reason: 'stitching use only' })], merge: { ... } }, { schema: usersSchema, - transforms: [new RemoveDeprecations('gateway access only')], + transforms: [new RemoveDeprecations({ reason: 'gateway access only' })], merge: { ... } }, ], From 0eb33a78a34c9d010529bb465c043b8c6a16e7df Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 10:48:33 -0400 Subject: [PATCH 05/28] eat more dogfood. --- .../src/transforms/RemoveDeprecatedFields.ts | 14 +++++--- .../src/transforms/RemoveDeprecations.ts | 32 ++++++++++--------- .../tests/transformDeprecations.test.ts | 3 ++ 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts b/packages/stitch/src/transforms/RemoveDeprecatedFields.ts index c752c914c71..948f27162d0 100644 --- a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts +++ b/packages/stitch/src/transforms/RemoveDeprecatedFields.ts @@ -1,16 +1,20 @@ import { GraphQLSchema } from 'graphql'; -import { Transform, MapperKind, mapSchema } from '@graphql-tools/utils'; +import { Transform } from '@graphql-tools/utils'; +import { FilterObjectFields } from '@graphql-tools/wrap'; export class RemoveDeprecatedFields implements Transform { private readonly reason: string; constructor({ reason }: { reason: string }) { this.reason = reason; + this.transformer = new FilterObjectFields( + (typeName: string, fieldName: string, fieldConfig: GraphQLFieldConfig) => { + return fieldConfig.deprecationReason === this.reason ? undefined : null; + } + ); } - public transformSchema(schema: GraphQLSchema): GraphQLSchema { - return mapSchema(schema, { - [MapperKind.FIELD]: field => (field.deprecationReason === this.reason ? undefined : null), - }); + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return this.transformer.transformSchema(originalSchema); } } diff --git a/packages/stitch/src/transforms/RemoveDeprecations.ts b/packages/stitch/src/transforms/RemoveDeprecations.ts index ef2252f3409..f78619256d3 100644 --- a/packages/stitch/src/transforms/RemoveDeprecations.ts +++ b/packages/stitch/src/transforms/RemoveDeprecations.ts @@ -1,28 +1,30 @@ import { GraphQLSchema } from 'graphql'; -import { Transform, MapperKind, mapSchema } from '@graphql-tools/utils'; +import { Transform } from '@graphql-tools/utils'; +import { TransformObjectFields } from '@graphql-tools/wrap'; export class RemoveDeprecations implements Transform { private readonly reason: string; constructor({ reason }: { reason: string }) { this.reason = reason; - } - - public transformSchema(schema: GraphQLSchema): GraphQLSchema { - return mapSchema(schema, { - [MapperKind.FIELD]: field => { - if (field.deprecationReason === this.reason) { - field = { - ...field, + this.transformer = new TransformObjectFields( + (typeName: string, fieldName: string, fieldConfig: GraphQLFieldConfig) => { + if (fieldConfig.deprecationReason === this.reason) { + fieldConfig = { + ...fieldConfig, astNode: { - ...field.astNode, - directives: field.astNode.directives.filter(dir => dir.name.value !== 'deprecated'), + ...fieldConfig.astNode, + directives: fieldConfig.astNode.directives.filter(dir => dir.name.value !== 'deprecated'), }, }; - delete field.deprecationReason; - return field; + delete fieldConfig.deprecationReason; + return fieldConfig; } - }, - }); + } + ); + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return this.transformer.transformSchema(originalSchema); } } diff --git a/packages/stitch/tests/transformDeprecations.test.ts b/packages/stitch/tests/transformDeprecations.test.ts index 3cd62696f56..a8d29e080ca 100644 --- a/packages/stitch/tests/transformDeprecations.test.ts +++ b/packages/stitch/tests/transformDeprecations.test.ts @@ -23,6 +23,9 @@ describe('transform deprecations', () => { email: String! @deprecated(reason: "other deprecation") } + input List { + thing: String + } type Listing { seller: User! @deprecated(reason: "gateway access only") buyer: User @deprecated(reason: "gateway access only") From a543d19c74f7fcf8629ecc40cc9492fbb6a02cf5 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 11:05:46 -0400 Subject: [PATCH 06/28] im terrible at typescript. --- packages/stitch/src/transforms/RemoveDeprecatedFields.ts | 5 +++-- packages/stitch/src/transforms/RemoveDeprecations.ts | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts b/packages/stitch/src/transforms/RemoveDeprecatedFields.ts index 948f27162d0..57d156382df 100644 --- a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts +++ b/packages/stitch/src/transforms/RemoveDeprecatedFields.ts @@ -1,14 +1,15 @@ -import { GraphQLSchema } from 'graphql'; +import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; import { Transform } from '@graphql-tools/utils'; import { FilterObjectFields } from '@graphql-tools/wrap'; export class RemoveDeprecatedFields implements Transform { private readonly reason: string; + private readonly transformer: FilterObjectFields; constructor({ reason }: { reason: string }) { this.reason = reason; this.transformer = new FilterObjectFields( - (typeName: string, fieldName: string, fieldConfig: GraphQLFieldConfig) => { + (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { return fieldConfig.deprecationReason === this.reason ? undefined : null; } ); diff --git a/packages/stitch/src/transforms/RemoveDeprecations.ts b/packages/stitch/src/transforms/RemoveDeprecations.ts index f78619256d3..ce16b875f97 100644 --- a/packages/stitch/src/transforms/RemoveDeprecations.ts +++ b/packages/stitch/src/transforms/RemoveDeprecations.ts @@ -1,20 +1,23 @@ -import { GraphQLSchema } from 'graphql'; +import { GraphQLSchema, GraphQLFieldConfig, DirectiveDefinitionNode } from 'graphql'; import { Transform } from '@graphql-tools/utils'; import { TransformObjectFields } from '@graphql-tools/wrap'; export class RemoveDeprecations implements Transform { private readonly reason: string; + private readonly transformer: TransformObjectFields; constructor({ reason }: { reason: string }) { this.reason = reason; this.transformer = new TransformObjectFields( - (typeName: string, fieldName: string, fieldConfig: GraphQLFieldConfig) => { + (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { if (fieldConfig.deprecationReason === this.reason) { fieldConfig = { ...fieldConfig, astNode: { ...fieldConfig.astNode, - directives: fieldConfig.astNode.directives.filter(dir => dir.name.value !== 'deprecated'), + directives: fieldConfig.astNode.directives.filter( + (dir: DirectiveDefinitionNode) => dir.name.value !== 'deprecated' + ), }, }; delete fieldConfig.deprecationReason; From 713154be15fb4d9ef05d89e56510c8bc3726cf27 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 15:11:57 -0400 Subject: [PATCH 07/28] refactor into respective libs. --- packages/stitch/src/index.ts | 4 +-- .../src/transforms/RemoveDeprecations.ts | 33 ------------------ ...atedFields.ts => RemoveDirectiveFields.ts} | 10 +++--- .../stitch/src/transforms/RemoveDirectives.ts | 15 ++++++++ ...ns.test.ts => transformDirectives.test.ts} | 6 ++-- packages/utils/src/index.ts | 1 + packages/utils/src/matchDirective.ts | 19 +++++++++++ packages/utils/tests/matchDirective.test.ts | 34 +++++++++++++++++++ .../src/transforms/FilterFieldDirectives.ts | 34 +++++++++++++++++++ packages/wrap/src/transforms/index.ts | 1 + website/docs/stitch-type-merging.md | 6 ++-- 11 files changed, 116 insertions(+), 47 deletions(-) delete mode 100644 packages/stitch/src/transforms/RemoveDeprecations.ts rename packages/stitch/src/transforms/{RemoveDeprecatedFields.ts => RemoveDirectiveFields.ts} (60%) create mode 100644 packages/stitch/src/transforms/RemoveDirectives.ts rename packages/stitch/tests/{transformDeprecations.test.ts => transformDirectives.test.ts} (87%) create mode 100644 packages/utils/src/matchDirective.ts create mode 100644 packages/utils/tests/matchDirective.test.ts create mode 100644 packages/wrap/src/transforms/FilterFieldDirectives.ts diff --git a/packages/stitch/src/index.ts b/packages/stitch/src/index.ts index c5a95241f50..312a99d0556 100755 --- a/packages/stitch/src/index.ts +++ b/packages/stitch/src/index.ts @@ -1,4 +1,4 @@ export { stitchSchemas } from './stitchSchemas'; export { forwardArgsToSelectionSet } from './selectionSetArgs'; -export { RemoveDeprecations } from './transforms/RemoveDeprecations'; -export { RemoveDeprecatedFields } from './transforms/RemoveDeprecatedFields'; +export { default as RemoveDirectives } from './transforms/RemoveDirectives'; +export { default as RemoveDirectiveFields } from './transforms/RemoveDirectiveFields'; diff --git a/packages/stitch/src/transforms/RemoveDeprecations.ts b/packages/stitch/src/transforms/RemoveDeprecations.ts deleted file mode 100644 index ce16b875f97..00000000000 --- a/packages/stitch/src/transforms/RemoveDeprecations.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { GraphQLSchema, GraphQLFieldConfig, DirectiveDefinitionNode } from 'graphql'; -import { Transform } from '@graphql-tools/utils'; -import { TransformObjectFields } from '@graphql-tools/wrap'; - -export class RemoveDeprecations implements Transform { - private readonly reason: string; - private readonly transformer: TransformObjectFields; - - constructor({ reason }: { reason: string }) { - this.reason = reason; - this.transformer = new TransformObjectFields( - (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { - if (fieldConfig.deprecationReason === this.reason) { - fieldConfig = { - ...fieldConfig, - astNode: { - ...fieldConfig.astNode, - directives: fieldConfig.astNode.directives.filter( - (dir: DirectiveDefinitionNode) => dir.name.value !== 'deprecated' - ), - }, - }; - delete fieldConfig.deprecationReason; - return fieldConfig; - } - } - ); - } - - public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { - return this.transformer.transformSchema(originalSchema); - } -} diff --git a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts b/packages/stitch/src/transforms/RemoveDirectiveFields.ts similarity index 60% rename from packages/stitch/src/transforms/RemoveDeprecatedFields.ts rename to packages/stitch/src/transforms/RemoveDirectiveFields.ts index 57d156382df..fbd01ffbb4e 100644 --- a/packages/stitch/src/transforms/RemoveDeprecatedFields.ts +++ b/packages/stitch/src/transforms/RemoveDirectiveFields.ts @@ -1,16 +1,14 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; -import { Transform } from '@graphql-tools/utils'; +import { Transform, matchDirective } from '@graphql-tools/utils'; import { FilterObjectFields } from '@graphql-tools/wrap'; -export class RemoveDeprecatedFields implements Transform { - private readonly reason: string; +export default class RemoveDirectiveFields implements Transform { private readonly transformer: FilterObjectFields; - constructor({ reason }: { reason: string }) { - this.reason = reason; + constructor(directiveName: string, args: Record = {}) { this.transformer = new FilterObjectFields( (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { - return fieldConfig.deprecationReason === this.reason ? undefined : null; + return !!fieldConfig.astNode.directives.find(dir => matchDirective(dir, directiveName, args)); } ); } diff --git a/packages/stitch/src/transforms/RemoveDirectives.ts b/packages/stitch/src/transforms/RemoveDirectives.ts new file mode 100644 index 00000000000..83c4aa572bb --- /dev/null +++ b/packages/stitch/src/transforms/RemoveDirectives.ts @@ -0,0 +1,15 @@ +import { GraphQLSchema, DirectiveNode } from 'graphql'; +import { Transform, matchDirective } from '@graphql-tools/utils'; +import { FilterFieldDirectives } from '@graphql-tools/wrap'; + +export default class RemoveDirectives implements Transform { + private readonly transformer: FilterFieldDirectives; + + constructor(directiveName: string, args: Record = {}) { + this.transformer = new FilterFieldDirectives((dir: DirectiveNode) => !matchDirective(dir, directiveName, args)); + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return this.transformer.transformSchema(originalSchema); + } +} diff --git a/packages/stitch/tests/transformDeprecations.test.ts b/packages/stitch/tests/transformDirectives.test.ts similarity index 87% rename from packages/stitch/tests/transformDeprecations.test.ts rename to packages/stitch/tests/transformDirectives.test.ts index a8d29e080ca..3bf289402aa 100644 --- a/packages/stitch/tests/transformDeprecations.test.ts +++ b/packages/stitch/tests/transformDirectives.test.ts @@ -1,6 +1,6 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '../src/stitchSchemas'; -import { RemoveDeprecations, RemoveDeprecatedFields } from '../src/index'; +import { RemoveDirectives, RemoveDirectiveFields } from '../src/index'; describe('transform deprecations', () => { test('removes specific deprecations and deprecated fields from the gateway schema', async () => { @@ -37,11 +37,11 @@ describe('transform deprecations', () => { subschemas: [ { schema: listingsSchema, - transforms: [new RemoveDeprecatedFields({ reason: 'stitching use only' })] + transforms: [new RemoveDirectiveFields('deprecated', { reason: 'gateway access only' })] }, { schema: usersSchema, - transforms: [new RemoveDeprecations({ reason: 'gateway access only' })] + transforms: [new RemoveDirectives('deprecated', { reason: 'gateway access only' })] }, ], }); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 734687b9297..e40194e3656 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -48,3 +48,4 @@ export * from './errors'; export * from './toConfig'; export * from './observableToAsyncIterable'; export * from './visitResult'; +export * from './matchDirective'; diff --git a/packages/utils/src/matchDirective.ts b/packages/utils/src/matchDirective.ts new file mode 100644 index 00000000000..22ed0ee9e82 --- /dev/null +++ b/packages/utils/src/matchDirective.ts @@ -0,0 +1,19 @@ +export function matchDirective( + directive: DirectiveNode, + requiredName: string, + requiredArgs: Record = {} +): boolean { + if (directive.name.value === requiredName) { + const requiredArgNames = Object.keys(requiredArgs); + + if (requiredArgNames.length) { + const argsMap: Record = Object.create(null); + directive.arguments.forEach(arg => { + argsMap[arg.name.value] = arg.value.value; + }); + return requiredArgNames.every(name => requiredArgs[name] === argsMap[name]); + } + return true; + } + return false; +} diff --git a/packages/utils/tests/matchDirective.test.ts b/packages/utils/tests/matchDirective.test.ts new file mode 100644 index 00000000000..c758339fedc --- /dev/null +++ b/packages/utils/tests/matchDirective.test.ts @@ -0,0 +1,34 @@ +import { matchDirective } from '../src/matchDirective'; + +describe('matchDirective', () => { + test('matches a directive node based on flexible criteria', () => { + const dir = { + kind: 'Directive', + name: { kind: 'Name', value: 'deprecated' }, + arguments: [{ + kind: 'Argument', + name: { kind: 'Name', value: 'reason' }, + value: { + kind: 'StringValue', + value: 'reason', + block: false, + } + }, { + kind: 'Argument', + name: { kind: 'Name', value: 'also' }, + value: { + kind: 'StringValue', + value: 'also', + block: false, + } + }] + }; + + expect(matchDirective(dir, 'notthis')).toBe(false); + expect(matchDirective(dir, 'deprecated')).toBe(true); + expect(matchDirective(dir, 'deprecated', { reason: 'reason' })).toBe(true); + expect(matchDirective(dir, 'deprecated', { reason: 'reason', also: 'also' })).toBe(true); + expect(matchDirective(dir, 'deprecated', { reason: 'reason', and: 'and' })).toBe(false); + expect(matchDirective(dir, 'deprecated', { this: 'this' })).toBe(false); + }); +}); diff --git a/packages/wrap/src/transforms/FilterFieldDirectives.ts b/packages/wrap/src/transforms/FilterFieldDirectives.ts new file mode 100644 index 00000000000..173616b95b1 --- /dev/null +++ b/packages/wrap/src/transforms/FilterFieldDirectives.ts @@ -0,0 +1,34 @@ +import { GraphQLSchema, GraphQLFieldConfig, DirectiveNode } from 'graphql'; +import { Transform } from '@graphql-tools/utils'; +import TransformObjectFields from './TransformObjectFields'; + +export default class FilterFieldDirectives implements Transform { + private readonly transformer: TransformObjectFields; + + constructor(filter: (dir: DirectiveNode) => boolean) { + this.transformer = new TransformObjectFields( + (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { + const keepDirectives = fieldConfig.astNode.directives.filter(dir => filter(dir)); + + if (keepDirectives.length !== fieldConfig.astNode.directives.length) { + fieldConfig = { + ...fieldConfig, + astNode: { + ...fieldConfig.astNode, + directives: keepDirectives, + }, + }; + + if (fieldConfig.deprecationReason && !keepDirectives.some(dir => dir.name.value === 'deprecated')) { + delete fieldConfig.deprecationReason; + } + return fieldConfig; + } + } + ); + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return this.transformer.transformSchema(originalSchema); + } +} diff --git a/packages/wrap/src/transforms/index.ts b/packages/wrap/src/transforms/index.ts index 1bae4f02ae7..853d094d2fb 100644 --- a/packages/wrap/src/transforms/index.ts +++ b/packages/wrap/src/transforms/index.ts @@ -14,6 +14,7 @@ export { default as FilterInterfaceFields } from './FilterInterfaceFields'; export { default as TransformInputObjectFields } from './TransformInputObjectFields'; export { default as RenameInputObjectFields } from './RenameInputObjectFields'; export { default as FilterInputObjectFields } from './FilterInputObjectFields'; +export { default as FilterFieldDirectives } from './FilterFieldDirectives'; export { default as MapLeafValues } from './MapLeafValues'; export { default as TransformEnumValues } from './TransformEnumValues'; export { default as TransformQuery } from './TransformQuery'; diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index dd426cb04c1..ef3f5aab26c 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -397,7 +397,7 @@ However, you may notice that both `buyerId` and `sellerId` keys are _always_ req One disadvantage of this pattern is that we end up with clutter—`buyerId`/`sellerId` are extra fields, and `buyer`/`seller` fields have gateway dependencies. To tidy things up, we can aggressively deprecate these fields in subschemas and then remove/normalize their behavior in the gateway using available transforms: ```js -import { RemoveDeprecatedFields, RemoveDeprecations } from '@graphql-tools/stitch'; +import { RemoveDirectiveFields, RemoveDirectives } from '@graphql-tools/stitch'; const listingsSchema = makeExecutableSchema({ typeDefs: ` @@ -429,12 +429,12 @@ const gatewaySchema = stitchSchemas({ subschemas: [ { schema: listingsSchema, - transforms: [new RemoveDeprecatedFields({ reason: 'stitching use only' })], + transforms: [new RemoveDirectiveFields('deprecated', { reason: 'gateway access only' })], merge: { ... } }, { schema: usersSchema, - transforms: [new RemoveDeprecations({ reason: 'gateway access only' })], + transforms: [new RemoveDirectives('deprecated', { reason: 'gateway access only' })], merge: { ... } }, ], From ea3778e98dd0487f4dc22e8c57aa4367b78e04a5 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 15:16:43 -0400 Subject: [PATCH 08/28] yeah, whoops. --- packages/utils/src/matchDirective.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/matchDirective.ts b/packages/utils/src/matchDirective.ts index 22ed0ee9e82..c492f171951 100644 --- a/packages/utils/src/matchDirective.ts +++ b/packages/utils/src/matchDirective.ts @@ -1,3 +1,5 @@ +import { DirectiveNode, ArgumentNode } from 'graphql'; + export function matchDirective( directive: DirectiveNode, requiredName: string, @@ -8,7 +10,7 @@ export function matchDirective( if (requiredArgNames.length) { const argsMap: Record = Object.create(null); - directive.arguments.forEach(arg => { + directive.arguments.forEach((arg: ArgumentNode) => { argsMap[arg.name.value] = arg.value.value; }); return requiredArgNames.every(name => requiredArgs[name] === argsMap[name]); From bceead6001068f530fc43bd4f71b72e412991435 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 15:32:48 -0400 Subject: [PATCH 09/28] is this right? --- packages/utils/src/matchDirective.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/matchDirective.ts b/packages/utils/src/matchDirective.ts index c492f171951..d3d4353ea43 100644 --- a/packages/utils/src/matchDirective.ts +++ b/packages/utils/src/matchDirective.ts @@ -1,4 +1,4 @@ -import { DirectiveNode, ArgumentNode } from 'graphql'; +import { DirectiveNode, ArgumentNode, StringValueNode } from 'graphql'; export function matchDirective( directive: DirectiveNode, @@ -11,7 +11,8 @@ export function matchDirective( if (requiredArgNames.length) { const argsMap: Record = Object.create(null); directive.arguments.forEach((arg: ArgumentNode) => { - argsMap[arg.name.value] = arg.value.value; + const argValue = arg.value as StringValueNode; + argsMap[arg.name.value] = argValue.value; }); return requiredArgNames.every(name => requiredArgs[name] === argsMap[name]); } From e7c63582158a3c42c92e17d64be828656a8fb7d1 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 15:49:35 -0400 Subject: [PATCH 10/28] also check the ast composition. --- packages/stitch/tests/transformDirectives.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/stitch/tests/transformDirectives.test.ts b/packages/stitch/tests/transformDirectives.test.ts index 3bf289402aa..aeb6259b0f4 100644 --- a/packages/stitch/tests/transformDirectives.test.ts +++ b/packages/stitch/tests/transformDirectives.test.ts @@ -3,7 +3,7 @@ import { stitchSchemas } from '../src/stitchSchemas'; import { RemoveDirectives, RemoveDirectiveFields } from '../src/index'; describe('transform deprecations', () => { - test('removes specific deprecations and deprecated fields from the gateway schema', async () => { + test('removes directives with arguments, includes deprecations', async () => { const listingsSchema = makeExecutableSchema({ typeDefs: ` type Listing { @@ -22,10 +22,6 @@ describe('transform deprecations', () => { id: ID! email: String! @deprecated(reason: "other deprecation") } - - input List { - thing: String - } type Listing { seller: User! @deprecated(reason: "gateway access only") buyer: User @deprecated(reason: "gateway access only") @@ -50,9 +46,13 @@ describe('transform deprecations', () => { expect(gatewaySchema.getType('Listing').getFields().sellerId).toBe(undefined); expect(usersSchema.getType('Listing').getFields().seller.deprecationReason).toBe('gateway access only'); + expect(usersSchema.getType('Listing').getFields().seller.astNode.directives.length).toEqual(1); expect(gatewaySchema.getType('Listing').getFields().seller.deprecationReason).toBe(undefined); + expect(gatewaySchema.getType('Listing').getFields().seller.astNode.directives.length).toEqual(0); expect(usersSchema.getType('User').getFields().email.deprecationReason).toBe('other deprecation'); + expect(usersSchema.getType('User').getFields().email.astNode.directives.length).toEqual(1); expect(gatewaySchema.getType('User').getFields().email.deprecationReason).toBe('other deprecation'); + expect(gatewaySchema.getType('User').getFields().email.astNode.directives.length).toEqual(1); }); }); From 48ed54ce76ee77aae0cfcc0068912c2a1a25034e Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 16:27:10 -0400 Subject: [PATCH 11/28] fix rogue typing. --- packages/stitch/tests/transformDirectives.test.ts | 3 +++ packages/utils/src/matchDirective.ts | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/stitch/tests/transformDirectives.test.ts b/packages/stitch/tests/transformDirectives.test.ts index aeb6259b0f4..d1d7fdcbe63 100644 --- a/packages/stitch/tests/transformDirectives.test.ts +++ b/packages/stitch/tests/transformDirectives.test.ts @@ -1,4 +1,5 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; +import { getDirectives } from '@graphql-tools/utils'; import { stitchSchemas } from '../src/stitchSchemas'; import { RemoveDirectives, RemoveDirectiveFields } from '../src/index'; @@ -29,6 +30,8 @@ describe('transform deprecations', () => { ` }); + console.log(getDirectives(usersSchema)) + const gatewaySchema = stitchSchemas({ subschemas: [ { diff --git a/packages/utils/src/matchDirective.ts b/packages/utils/src/matchDirective.ts index d3d4353ea43..eef5bc640b1 100644 --- a/packages/utils/src/matchDirective.ts +++ b/packages/utils/src/matchDirective.ts @@ -1,4 +1,4 @@ -import { DirectiveNode, ArgumentNode, StringValueNode } from 'graphql'; +import { DirectiveNode, ArgumentNode } from 'graphql'; export function matchDirective( directive: DirectiveNode, @@ -11,8 +11,7 @@ export function matchDirective( if (requiredArgNames.length) { const argsMap: Record = Object.create(null); directive.arguments.forEach((arg: ArgumentNode) => { - const argValue = arg.value as StringValueNode; - argsMap[arg.name.value] = argValue.value; + argsMap[arg.name.value] = 'value' in arg.value ? arg.value.value : undefined; }); return requiredArgNames.every(name => requiredArgs[name] === argsMap[name]); } From 3d405e07e36d21ab75db5b9a6631d6be0aefaf16 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 16:28:01 -0400 Subject: [PATCH 12/28] shrink diff. --- packages/stitch/tests/transformDirectives.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/stitch/tests/transformDirectives.test.ts b/packages/stitch/tests/transformDirectives.test.ts index d1d7fdcbe63..f1354326e95 100644 --- a/packages/stitch/tests/transformDirectives.test.ts +++ b/packages/stitch/tests/transformDirectives.test.ts @@ -1,5 +1,4 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; -import { getDirectives } from '@graphql-tools/utils'; import { stitchSchemas } from '../src/stitchSchemas'; import { RemoveDirectives, RemoveDirectiveFields } from '../src/index'; @@ -30,7 +29,7 @@ describe('transform deprecations', () => { ` }); - console.log(getDirectives(usersSchema)) + const gatewaySchema = stitchSchemas({ subschemas: [ From 415773cd9d3528555efbf73c6f73418425e8ee45 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 16:37:12 -0400 Subject: [PATCH 13/28] rename RemoveFieldsWithDirective. --- packages/stitch/src/index.ts | 2 +- ...{RemoveDirectiveFields.ts => RemoveFieldsWithDirective.ts} | 2 +- packages/stitch/tests/transformDirectives.test.ts | 4 ++-- website/docs/stitch-type-merging.md | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename packages/stitch/src/transforms/{RemoveDirectiveFields.ts => RemoveFieldsWithDirective.ts} (91%) diff --git a/packages/stitch/src/index.ts b/packages/stitch/src/index.ts index 312a99d0556..1765d6f1f91 100755 --- a/packages/stitch/src/index.ts +++ b/packages/stitch/src/index.ts @@ -1,4 +1,4 @@ export { stitchSchemas } from './stitchSchemas'; export { forwardArgsToSelectionSet } from './selectionSetArgs'; export { default as RemoveDirectives } from './transforms/RemoveDirectives'; -export { default as RemoveDirectiveFields } from './transforms/RemoveDirectiveFields'; +export { default as RemoveFieldsWithDirective } from './transforms/RemoveFieldsWithDirective'; diff --git a/packages/stitch/src/transforms/RemoveDirectiveFields.ts b/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts similarity index 91% rename from packages/stitch/src/transforms/RemoveDirectiveFields.ts rename to packages/stitch/src/transforms/RemoveFieldsWithDirective.ts index fbd01ffbb4e..c6e3f16894b 100644 --- a/packages/stitch/src/transforms/RemoveDirectiveFields.ts +++ b/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts @@ -2,7 +2,7 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; import { Transform, matchDirective } from '@graphql-tools/utils'; import { FilterObjectFields } from '@graphql-tools/wrap'; -export default class RemoveDirectiveFields implements Transform { +export default class RemoveFieldsWithDirective implements Transform { private readonly transformer: FilterObjectFields; constructor(directiveName: string, args: Record = {}) { diff --git a/packages/stitch/tests/transformDirectives.test.ts b/packages/stitch/tests/transformDirectives.test.ts index f1354326e95..c8b1a38de03 100644 --- a/packages/stitch/tests/transformDirectives.test.ts +++ b/packages/stitch/tests/transformDirectives.test.ts @@ -1,6 +1,6 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '../src/stitchSchemas'; -import { RemoveDirectives, RemoveDirectiveFields } from '../src/index'; +import { RemoveDirectives, RemoveFieldsWithDirective } from '../src/index'; describe('transform deprecations', () => { test('removes directives with arguments, includes deprecations', async () => { @@ -35,7 +35,7 @@ describe('transform deprecations', () => { subschemas: [ { schema: listingsSchema, - transforms: [new RemoveDirectiveFields('deprecated', { reason: 'gateway access only' })] + transforms: [new RemoveFieldsWithDirective('deprecated', { reason: 'gateway access only' })] }, { schema: usersSchema, diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index ef3f5aab26c..66ae3d5cfd5 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -397,7 +397,7 @@ However, you may notice that both `buyerId` and `sellerId` keys are _always_ req One disadvantage of this pattern is that we end up with clutter—`buyerId`/`sellerId` are extra fields, and `buyer`/`seller` fields have gateway dependencies. To tidy things up, we can aggressively deprecate these fields in subschemas and then remove/normalize their behavior in the gateway using available transforms: ```js -import { RemoveDirectiveFields, RemoveDirectives } from '@graphql-tools/stitch'; +import { RemoveFieldsWithDirective, RemoveDirectives } from '@graphql-tools/stitch'; const listingsSchema = makeExecutableSchema({ typeDefs: ` @@ -429,7 +429,7 @@ const gatewaySchema = stitchSchemas({ subschemas: [ { schema: listingsSchema, - transforms: [new RemoveDirectiveFields('deprecated', { reason: 'gateway access only' })], + transforms: [new RemoveFieldsWithDirective('deprecated', { reason: 'gateway access only' })], merge: { ... } }, { From 67e931d8cfd8380dcdac7ca50dd6c1f1b615737f Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 16:40:32 -0400 Subject: [PATCH 14/28] another better name. --- packages/stitch/src/index.ts | 2 +- .../{RemoveDirectives.ts => RemoveFieldDirectives.ts} | 2 +- packages/stitch/tests/transformDirectives.test.ts | 4 ++-- website/docs/stitch-type-merging.md | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename packages/stitch/src/transforms/{RemoveDirectives.ts => RemoveFieldDirectives.ts} (89%) diff --git a/packages/stitch/src/index.ts b/packages/stitch/src/index.ts index 1765d6f1f91..7a713c3c06a 100755 --- a/packages/stitch/src/index.ts +++ b/packages/stitch/src/index.ts @@ -1,4 +1,4 @@ export { stitchSchemas } from './stitchSchemas'; export { forwardArgsToSelectionSet } from './selectionSetArgs'; -export { default as RemoveDirectives } from './transforms/RemoveDirectives'; +export { default as RemoveFieldDirectives } from './transforms/RemoveFieldDirectives'; export { default as RemoveFieldsWithDirective } from './transforms/RemoveFieldsWithDirective'; diff --git a/packages/stitch/src/transforms/RemoveDirectives.ts b/packages/stitch/src/transforms/RemoveFieldDirectives.ts similarity index 89% rename from packages/stitch/src/transforms/RemoveDirectives.ts rename to packages/stitch/src/transforms/RemoveFieldDirectives.ts index 83c4aa572bb..b86ba5014d2 100644 --- a/packages/stitch/src/transforms/RemoveDirectives.ts +++ b/packages/stitch/src/transforms/RemoveFieldDirectives.ts @@ -2,7 +2,7 @@ import { GraphQLSchema, DirectiveNode } from 'graphql'; import { Transform, matchDirective } from '@graphql-tools/utils'; import { FilterFieldDirectives } from '@graphql-tools/wrap'; -export default class RemoveDirectives implements Transform { +export default class RemoveFieldDirectives implements Transform { private readonly transformer: FilterFieldDirectives; constructor(directiveName: string, args: Record = {}) { diff --git a/packages/stitch/tests/transformDirectives.test.ts b/packages/stitch/tests/transformDirectives.test.ts index c8b1a38de03..c21ea24e00c 100644 --- a/packages/stitch/tests/transformDirectives.test.ts +++ b/packages/stitch/tests/transformDirectives.test.ts @@ -1,6 +1,6 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '../src/stitchSchemas'; -import { RemoveDirectives, RemoveFieldsWithDirective } from '../src/index'; +import { RemoveFieldDirectives, RemoveFieldsWithDirective } from '../src/index'; describe('transform deprecations', () => { test('removes directives with arguments, includes deprecations', async () => { @@ -39,7 +39,7 @@ describe('transform deprecations', () => { }, { schema: usersSchema, - transforms: [new RemoveDirectives('deprecated', { reason: 'gateway access only' })] + transforms: [new RemoveFieldDirectives('deprecated', { reason: 'gateway access only' })] }, ], }); diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index 66ae3d5cfd5..83cbe5d9c34 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -397,7 +397,7 @@ However, you may notice that both `buyerId` and `sellerId` keys are _always_ req One disadvantage of this pattern is that we end up with clutter—`buyerId`/`sellerId` are extra fields, and `buyer`/`seller` fields have gateway dependencies. To tidy things up, we can aggressively deprecate these fields in subschemas and then remove/normalize their behavior in the gateway using available transforms: ```js -import { RemoveFieldsWithDirective, RemoveDirectives } from '@graphql-tools/stitch'; +import { RemoveFieldsWithDirective, RemoveFieldDirectives } from '@graphql-tools/stitch'; const listingsSchema = makeExecutableSchema({ typeDefs: ` @@ -434,7 +434,7 @@ const gatewaySchema = stitchSchemas({ }, { schema: usersSchema, - transforms: [new RemoveDirectives('deprecated', { reason: 'gateway access only' })], + transforms: [new RemoveFieldDirectives('deprecated', { reason: 'gateway access only' })], merge: { ... } }, ], From 894478e47c50a23fd9a95431a2f6f46e8fbc6c18 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Wed, 19 Aug 2020 23:26:12 -0400 Subject: [PATCH 15/28] Fix documentation mistake --- website/docs/stitch-type-merging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index 83cbe5d9c34..22e744fd450 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -429,7 +429,7 @@ const gatewaySchema = stitchSchemas({ subschemas: [ { schema: listingsSchema, - transforms: [new RemoveFieldsWithDirective('deprecated', { reason: 'gateway access only' })], + transforms: [new RemoveFieldsWithDirective('deprecated', { reason: 'stitching use only' })], merge: { ... } }, { From 88f9cab076159e64c8091bf7edf9ee4046178c44 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Thu, 20 Aug 2020 08:24:06 -0400 Subject: [PATCH 16/28] fix incomplete test. --- packages/stitch/src/transforms/RemoveFieldsWithDirective.ts | 2 +- packages/stitch/tests/transformDirectives.test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts b/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts index c6e3f16894b..5a8b0d122bd 100644 --- a/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts +++ b/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts @@ -8,7 +8,7 @@ export default class RemoveFieldsWithDirective implements Transform { constructor(directiveName: string, args: Record = {}) { this.transformer = new FilterObjectFields( (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { - return !!fieldConfig.astNode.directives.find(dir => matchDirective(dir, directiveName, args)); + return !fieldConfig.astNode.directives.find(dir => matchDirective(dir, directiveName, args)); } ); } diff --git a/packages/stitch/tests/transformDirectives.test.ts b/packages/stitch/tests/transformDirectives.test.ts index c21ea24e00c..7da27619344 100644 --- a/packages/stitch/tests/transformDirectives.test.ts +++ b/packages/stitch/tests/transformDirectives.test.ts @@ -35,13 +35,14 @@ describe('transform deprecations', () => { subschemas: [ { schema: listingsSchema, - transforms: [new RemoveFieldsWithDirective('deprecated', { reason: 'gateway access only' })] + transforms: [new RemoveFieldsWithDirective('deprecated', { reason: 'stitching use only' })] }, { schema: usersSchema, transforms: [new RemoveFieldDirectives('deprecated', { reason: 'gateway access only' })] }, ], + mergeTypes: true }); expect(listingsSchema.getType('Listing').getFields().sellerId.deprecationReason).toBe('stitching use only'); From 1f7e24940b6a5070bc08831369b3ae7c1621fd45 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Thu, 20 Aug 2020 22:55:21 -0400 Subject: [PATCH 17/28] compare typed directives. --- .../src/transforms/RemoveFieldDirectives.ts | 8 +++-- .../transforms/RemoveFieldsWithDirective.ts | 21 ++++++++---- .../stitch/tests/transformDirectives.test.ts | 2 -- packages/utils/src/index.ts | 2 +- packages/utils/src/matchDirective.ts | 21 ------------ packages/utils/src/matchDirectiveValue.ts | 12 +++++++ packages/utils/tests/matchDirective.test.ts | 34 ------------------- .../utils/tests/matchDirectiveValue.test.ts | 26 ++++++++++++++ .../src/transforms/FilterFieldDirectives.ts | 23 ++++++++----- 9 files changed, 72 insertions(+), 77 deletions(-) delete mode 100644 packages/utils/src/matchDirective.ts create mode 100644 packages/utils/src/matchDirectiveValue.ts delete mode 100644 packages/utils/tests/matchDirective.test.ts create mode 100644 packages/utils/tests/matchDirectiveValue.test.ts diff --git a/packages/stitch/src/transforms/RemoveFieldDirectives.ts b/packages/stitch/src/transforms/RemoveFieldDirectives.ts index b86ba5014d2..62defbdc271 100644 --- a/packages/stitch/src/transforms/RemoveFieldDirectives.ts +++ b/packages/stitch/src/transforms/RemoveFieldDirectives.ts @@ -1,12 +1,14 @@ -import { GraphQLSchema, DirectiveNode } from 'graphql'; -import { Transform, matchDirective } from '@graphql-tools/utils'; +import { GraphQLSchema } from 'graphql'; +import { Transform, matchDirectiveValue } from '@graphql-tools/utils'; import { FilterFieldDirectives } from '@graphql-tools/wrap'; export default class RemoveFieldDirectives implements Transform { private readonly transformer: FilterFieldDirectives; constructor(directiveName: string, args: Record = {}) { - this.transformer = new FilterFieldDirectives((dir: DirectiveNode) => !matchDirective(dir, directiveName, args)); + this.transformer = new FilterFieldDirectives((dirName: string, dirValue: any) => { + return !(directiveName === dirName && matchDirectiveValue(dirValue, args)); + }); } public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { diff --git a/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts b/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts index 5a8b0d122bd..c3db348b86c 100644 --- a/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts +++ b/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts @@ -1,19 +1,26 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; -import { Transform, matchDirective } from '@graphql-tools/utils'; +import { Transform, getDirectives, matchDirectiveValue } from '@graphql-tools/utils'; import { FilterObjectFields } from '@graphql-tools/wrap'; export default class RemoveFieldsWithDirective implements Transform { - private readonly transformer: FilterObjectFields; + private readonly directiveName: string; + private readonly args: Record; constructor(directiveName: string, args: Record = {}) { - this.transformer = new FilterObjectFields( + this.directiveName = directiveName; + this.args = args; + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + const transformer = new FilterObjectFields( (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { - return !fieldConfig.astNode.directives.find(dir => matchDirective(dir, directiveName, args)); + const valueMap = getDirectives(originalSchema, fieldConfig); + return !fieldConfig.astNode.directives.find(dir => { + return dir.name.value === this.directiveName && matchDirectiveValue(valueMap[dir.name.value], this.args); + }); } ); - } - public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { - return this.transformer.transformSchema(originalSchema); + return transformer.transformSchema(originalSchema); } } diff --git a/packages/stitch/tests/transformDirectives.test.ts b/packages/stitch/tests/transformDirectives.test.ts index 7da27619344..07bcf3fe614 100644 --- a/packages/stitch/tests/transformDirectives.test.ts +++ b/packages/stitch/tests/transformDirectives.test.ts @@ -29,8 +29,6 @@ describe('transform deprecations', () => { ` }); - - const gatewaySchema = stitchSchemas({ subschemas: [ { diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index e40194e3656..9e0a5c67ef2 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -48,4 +48,4 @@ export * from './errors'; export * from './toConfig'; export * from './observableToAsyncIterable'; export * from './visitResult'; -export * from './matchDirective'; +export * from './matchDirectiveValue'; diff --git a/packages/utils/src/matchDirective.ts b/packages/utils/src/matchDirective.ts deleted file mode 100644 index eef5bc640b1..00000000000 --- a/packages/utils/src/matchDirective.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DirectiveNode, ArgumentNode } from 'graphql'; - -export function matchDirective( - directive: DirectiveNode, - requiredName: string, - requiredArgs: Record = {} -): boolean { - if (directive.name.value === requiredName) { - const requiredArgNames = Object.keys(requiredArgs); - - if (requiredArgNames.length) { - const argsMap: Record = Object.create(null); - directive.arguments.forEach((arg: ArgumentNode) => { - argsMap[arg.name.value] = 'value' in arg.value ? arg.value.value : undefined; - }); - return requiredArgNames.every(name => requiredArgs[name] === argsMap[name]); - } - return true; - } - return false; -} diff --git a/packages/utils/src/matchDirectiveValue.ts b/packages/utils/src/matchDirectiveValue.ts new file mode 100644 index 00000000000..45d33a60a77 --- /dev/null +++ b/packages/utils/src/matchDirectiveValue.ts @@ -0,0 +1,12 @@ +export function matchDirectiveValue(dirValue: any, criteria: Record) { + if (Array.isArray(dirValue)) { + return dirValue.some(val => matchesValue(val, criteria)); + } else if (dirValue) { + return matchesValue(dirValue, criteria); + } + return false; +} + +function matchesValue(dirValue: any, criteria: Record) { + return Object.keys(criteria).every(key => criteria[key] === dirValue[key]); +} diff --git a/packages/utils/tests/matchDirective.test.ts b/packages/utils/tests/matchDirective.test.ts deleted file mode 100644 index c758339fedc..00000000000 --- a/packages/utils/tests/matchDirective.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { matchDirective } from '../src/matchDirective'; - -describe('matchDirective', () => { - test('matches a directive node based on flexible criteria', () => { - const dir = { - kind: 'Directive', - name: { kind: 'Name', value: 'deprecated' }, - arguments: [{ - kind: 'Argument', - name: { kind: 'Name', value: 'reason' }, - value: { - kind: 'StringValue', - value: 'reason', - block: false, - } - }, { - kind: 'Argument', - name: { kind: 'Name', value: 'also' }, - value: { - kind: 'StringValue', - value: 'also', - block: false, - } - }] - }; - - expect(matchDirective(dir, 'notthis')).toBe(false); - expect(matchDirective(dir, 'deprecated')).toBe(true); - expect(matchDirective(dir, 'deprecated', { reason: 'reason' })).toBe(true); - expect(matchDirective(dir, 'deprecated', { reason: 'reason', also: 'also' })).toBe(true); - expect(matchDirective(dir, 'deprecated', { reason: 'reason', and: 'and' })).toBe(false); - expect(matchDirective(dir, 'deprecated', { this: 'this' })).toBe(false); - }); -}); diff --git a/packages/utils/tests/matchDirectiveValue.test.ts b/packages/utils/tests/matchDirectiveValue.test.ts new file mode 100644 index 00000000000..7736c2167e2 --- /dev/null +++ b/packages/utils/tests/matchDirectiveValue.test.ts @@ -0,0 +1,26 @@ +import { matchDirectiveValue } from '../src/index'; + +describe('matchDirectiveValue', () => { + test('matches directive value object', () => { + const dirValue = { reason: 'reason', also: 'also' }; + + expect(matchDirectiveValue(dirValue, { reason: 'reason' })).toBe(true); + expect(matchDirectiveValue(dirValue, { reason: 'reason', also: 'also' })).toBe(true); + expect(matchDirectiveValue(dirValue, { reason: 'reason', and: 'and' })).toBe(false); + expect(matchDirectiveValue(dirValue, { this: 'this' })).toBe(false); + expect(matchDirectiveValue(dirValue, { this: 'reason' })).toBe(false); + expect(matchDirectiveValue(dirValue, { reason: 'this' })).toBe(false); + }); + + test('matches directive value array', () => { + const dirValue = [{ reason: 'reason', also: 'also' }, { and: 'and' }]; + + expect(matchDirectiveValue(dirValue, { reason: 'reason' })).toBe(true); + expect(matchDirectiveValue(dirValue, { and: 'and' })).toBe(true); + expect(matchDirectiveValue(dirValue, { reason: 'reason', also: 'also' })).toBe(true); + expect(matchDirectiveValue(dirValue, { reason: 'reason', and: 'and' })).toBe(false); + expect(matchDirectiveValue(dirValue, { this: 'this' })).toBe(false); + expect(matchDirectiveValue(dirValue, { this: 'reason' })).toBe(false); + expect(matchDirectiveValue(dirValue, { reason: 'this' })).toBe(false); + }); +}); diff --git a/packages/wrap/src/transforms/FilterFieldDirectives.ts b/packages/wrap/src/transforms/FilterFieldDirectives.ts index 173616b95b1..d5a8db7539e 100644 --- a/packages/wrap/src/transforms/FilterFieldDirectives.ts +++ b/packages/wrap/src/transforms/FilterFieldDirectives.ts @@ -1,14 +1,21 @@ -import { GraphQLSchema, GraphQLFieldConfig, DirectiveNode } from 'graphql'; -import { Transform } from '@graphql-tools/utils'; +import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; +import { Transform, getDirectives } from '@graphql-tools/utils'; import TransformObjectFields from './TransformObjectFields'; export default class FilterFieldDirectives implements Transform { - private readonly transformer: TransformObjectFields; + private readonly filter: (dirName: string, dirValue: any) => boolean; - constructor(filter: (dir: DirectiveNode) => boolean) { - this.transformer = new TransformObjectFields( + 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) => { - const keepDirectives = fieldConfig.astNode.directives.filter(dir => filter(dir)); + const valueMap = getDirectives(originalSchema, fieldConfig); + const keepDirectives = fieldConfig.astNode.directives.filter(dir => + this.filter(dir.name.value, valueMap[dir.name.value]) + ); if (keepDirectives.length !== fieldConfig.astNode.directives.length) { fieldConfig = { @@ -26,9 +33,7 @@ export default class FilterFieldDirectives implements Transform { } } ); - } - public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { - return this.transformer.transformSchema(originalSchema); + return transformer.transformSchema(originalSchema); } } From d3dd95eb7d86c39709cf42f12055651ae74b4a10 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Thu, 20 Aug 2020 23:20:27 -0400 Subject: [PATCH 18/28] add tests for empty criteria. --- packages/utils/tests/matchDirectiveValue.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/utils/tests/matchDirectiveValue.test.ts b/packages/utils/tests/matchDirectiveValue.test.ts index 7736c2167e2..bfe395c5f03 100644 --- a/packages/utils/tests/matchDirectiveValue.test.ts +++ b/packages/utils/tests/matchDirectiveValue.test.ts @@ -4,6 +4,7 @@ describe('matchDirectiveValue', () => { test('matches directive value object', () => { const dirValue = { reason: 'reason', also: 'also' }; + expect(matchDirectiveValue(dirValue, {})).toBe(true); expect(matchDirectiveValue(dirValue, { reason: 'reason' })).toBe(true); expect(matchDirectiveValue(dirValue, { reason: 'reason', also: 'also' })).toBe(true); expect(matchDirectiveValue(dirValue, { reason: 'reason', and: 'and' })).toBe(false); @@ -15,6 +16,7 @@ describe('matchDirectiveValue', () => { test('matches directive value array', () => { const dirValue = [{ reason: 'reason', also: 'also' }, { and: 'and' }]; + expect(matchDirectiveValue(dirValue, {})).toBe(true); expect(matchDirectiveValue(dirValue, { reason: 'reason' })).toBe(true); expect(matchDirectiveValue(dirValue, { and: 'and' })).toBe(true); expect(matchDirectiveValue(dirValue, { reason: 'reason', also: 'also' })).toBe(true); @@ -23,4 +25,8 @@ describe('matchDirectiveValue', () => { expect(matchDirectiveValue(dirValue, { this: 'reason' })).toBe(false); expect(matchDirectiveValue(dirValue, { reason: 'this' })).toBe(false); }); + + test('matches empty arguments', () => { + expect(matchDirectiveValue({}, {})).toBe(true); + }); }); From d47ef154d35f2aad0583b58892af87c580baaa1a Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 21 Aug 2020 09:19:15 +0300 Subject: [PATCH 19/28] refactor * handle nested input object fields in directive arguments * handle repeatable directives * needs tests for transforms --- .../src/transforms/RemoveFieldDirectives.ts | 4 +-- .../transforms/RemoveFieldsWithDirective.ts | 12 ++++--- packages/utils/src/index.ts | 3 +- packages/utils/src/matchDirectiveValue.ts | 12 ------- packages/utils/src/valueMatchesCriteria.ts | 15 +++++++++ .../utils/tests/matchDirectiveValue.test.ts | 32 ------------------- packages/utils/tests/valueMatchesCriteria.ts | 19 +++++++++++ .../src/transforms/FilterFieldDirectives.ts | 11 ++++--- 8 files changed, 52 insertions(+), 56 deletions(-) delete mode 100644 packages/utils/src/matchDirectiveValue.ts create mode 100644 packages/utils/src/valueMatchesCriteria.ts delete mode 100644 packages/utils/tests/matchDirectiveValue.test.ts create mode 100644 packages/utils/tests/valueMatchesCriteria.ts diff --git a/packages/stitch/src/transforms/RemoveFieldDirectives.ts b/packages/stitch/src/transforms/RemoveFieldDirectives.ts index 62defbdc271..9929397eb53 100644 --- a/packages/stitch/src/transforms/RemoveFieldDirectives.ts +++ b/packages/stitch/src/transforms/RemoveFieldDirectives.ts @@ -1,5 +1,5 @@ import { GraphQLSchema } from 'graphql'; -import { Transform, matchDirectiveValue } from '@graphql-tools/utils'; +import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; import { FilterFieldDirectives } from '@graphql-tools/wrap'; export default class RemoveFieldDirectives implements Transform { @@ -7,7 +7,7 @@ export default class RemoveFieldDirectives implements Transform { constructor(directiveName: string, args: Record = {}) { this.transformer = new FilterFieldDirectives((dirName: string, dirValue: any) => { - return !(directiveName === dirName && matchDirectiveValue(dirValue, args)); + return !(directiveName === dirName && valueMatchesCriteria(dirValue, args)); }); } diff --git a/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts b/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts index c3db348b86c..28ce0b4c1d0 100644 --- a/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts +++ b/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts @@ -1,5 +1,5 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; -import { Transform, getDirectives, matchDirectiveValue } from '@graphql-tools/utils'; +import { Transform, getDirectives, valueMatchesCriteria } from '@graphql-tools/utils'; import { FilterObjectFields } from '@graphql-tools/wrap'; export default class RemoveFieldsWithDirective implements Transform { @@ -15,9 +15,13 @@ export default class RemoveFieldsWithDirective implements Transform { const transformer = new FilterObjectFields( (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { const valueMap = getDirectives(originalSchema, fieldConfig); - return !fieldConfig.astNode.directives.find(dir => { - return dir.name.value === this.directiveName && matchDirectiveValue(valueMap[dir.name.value], this.args); - }); + return !Object.keys(valueMap).some( + directiveName => + directiveName === this.directiveName && + (valueMatchesCriteria(valueMap[directiveName], this.args) || + (Array.isArray(valueMap[directiveName]) && + valueMap[directiveName].some((value: any) => valueMatchesCriteria(value, this.args)))) + ); } ); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 9e0a5c67ef2..1fda6639b9d 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -48,4 +48,5 @@ export * from './errors'; export * from './toConfig'; export * from './observableToAsyncIterable'; export * from './visitResult'; -export * from './matchDirectiveValue'; +export * from './getArgumentValues'; +export * from './valueMatchesCriteria'; diff --git a/packages/utils/src/matchDirectiveValue.ts b/packages/utils/src/matchDirectiveValue.ts deleted file mode 100644 index 45d33a60a77..00000000000 --- a/packages/utils/src/matchDirectiveValue.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function matchDirectiveValue(dirValue: any, criteria: Record) { - if (Array.isArray(dirValue)) { - return dirValue.some(val => matchesValue(val, criteria)); - } else if (dirValue) { - return matchesValue(dirValue, criteria); - } - return false; -} - -function matchesValue(dirValue: any, criteria: Record) { - return Object.keys(criteria).every(key => criteria[key] === dirValue[key]); -} diff --git a/packages/utils/src/valueMatchesCriteria.ts b/packages/utils/src/valueMatchesCriteria.ts new file mode 100644 index 00000000000..321ba91e1a8 --- /dev/null +++ b/packages/utils/src/valueMatchesCriteria.ts @@ -0,0 +1,15 @@ +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])) + ); + } + + return value === criteria; +} diff --git a/packages/utils/tests/matchDirectiveValue.test.ts b/packages/utils/tests/matchDirectiveValue.test.ts deleted file mode 100644 index bfe395c5f03..00000000000 --- a/packages/utils/tests/matchDirectiveValue.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { matchDirectiveValue } from '../src/index'; - -describe('matchDirectiveValue', () => { - test('matches directive value object', () => { - const dirValue = { reason: 'reason', also: 'also' }; - - expect(matchDirectiveValue(dirValue, {})).toBe(true); - expect(matchDirectiveValue(dirValue, { reason: 'reason' })).toBe(true); - expect(matchDirectiveValue(dirValue, { reason: 'reason', also: 'also' })).toBe(true); - expect(matchDirectiveValue(dirValue, { reason: 'reason', and: 'and' })).toBe(false); - expect(matchDirectiveValue(dirValue, { this: 'this' })).toBe(false); - expect(matchDirectiveValue(dirValue, { this: 'reason' })).toBe(false); - expect(matchDirectiveValue(dirValue, { reason: 'this' })).toBe(false); - }); - - test('matches directive value array', () => { - const dirValue = [{ reason: 'reason', also: 'also' }, { and: 'and' }]; - - expect(matchDirectiveValue(dirValue, {})).toBe(true); - expect(matchDirectiveValue(dirValue, { reason: 'reason' })).toBe(true); - expect(matchDirectiveValue(dirValue, { and: 'and' })).toBe(true); - expect(matchDirectiveValue(dirValue, { reason: 'reason', also: 'also' })).toBe(true); - expect(matchDirectiveValue(dirValue, { reason: 'reason', and: 'and' })).toBe(false); - expect(matchDirectiveValue(dirValue, { this: 'this' })).toBe(false); - expect(matchDirectiveValue(dirValue, { this: 'reason' })).toBe(false); - expect(matchDirectiveValue(dirValue, { reason: 'this' })).toBe(false); - }); - - test('matches empty arguments', () => { - expect(matchDirectiveValue({}, {})).toBe(true); - }); -}); diff --git a/packages/utils/tests/valueMatchesCriteria.ts b/packages/utils/tests/valueMatchesCriteria.ts new file mode 100644 index 00000000000..2a85927052c --- /dev/null +++ b/packages/utils/tests/valueMatchesCriteria.ts @@ -0,0 +1,19 @@ +import { valueMatchesCriteria } from '../src/index'; + +describe('valueMatchesCriteria', () => { + test('matches directive value object', () => { + 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, { this: 'reason' })).toBe(false); + expect(valueMatchesCriteria(dirValue, { reason: 'this' })).toBe(false); + }); + + test('matches empty arguments', () => { + expect(valueMatchesCriteria({}, {})).toBe(true); + }); +}); diff --git a/packages/wrap/src/transforms/FilterFieldDirectives.ts b/packages/wrap/src/transforms/FilterFieldDirectives.ts index d5a8db7539e..3b9587a584b 100644 --- a/packages/wrap/src/transforms/FilterFieldDirectives.ts +++ b/packages/wrap/src/transforms/FilterFieldDirectives.ts @@ -1,5 +1,5 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; -import { Transform, getDirectives } from '@graphql-tools/utils'; +import { Transform, getArgumentValues } from '@graphql-tools/utils'; import TransformObjectFields from './TransformObjectFields'; export default class FilterFieldDirectives implements Transform { @@ -12,10 +12,11 @@ export default class FilterFieldDirectives implements Transform { public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { const transformer = new TransformObjectFields( (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { - const valueMap = getDirectives(originalSchema, fieldConfig); - const keepDirectives = fieldConfig.astNode.directives.filter(dir => - this.filter(dir.name.value, valueMap[dir.name.value]) - ); + 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 = { From dbd92f5d8bc2f4551a20ec4a8b5de237a2cac7bc Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 21 Aug 2020 09:35:03 +0300 Subject: [PATCH 20/28] small fix --- packages/stitch/src/transforms/RemoveFieldsWithDirective.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts b/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts index 28ce0b4c1d0..772f526dbf0 100644 --- a/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts +++ b/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts @@ -18,9 +18,9 @@ export default class RemoveFieldsWithDirective implements Transform { return !Object.keys(valueMap).some( directiveName => directiveName === this.directiveName && - (valueMatchesCriteria(valueMap[directiveName], this.args) || - (Array.isArray(valueMap[directiveName]) && - valueMap[directiveName].some((value: any) => valueMatchesCriteria(value, this.args)))) + ((Array.isArray(valueMap[directiveName]) && + valueMap[directiveName].some((value: any) => valueMatchesCriteria(value, this.args))) || + valueMatchesCriteria(valueMap[directiveName], this.args)) ); } ); From 52480d78f02fab188a17b51aefcb813b7719bc16 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Fri, 21 Aug 2020 23:04:09 -0400 Subject: [PATCH 21/28] add FilterFieldDirectives tests. --- packages/wrap/tests/transforms.test.ts | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/wrap/tests/transforms.test.ts b/packages/wrap/tests/transforms.test.ts index 6275804313e..64cbb38a4e4 100644 --- a/packages/wrap/tests/transforms.test.ts +++ b/packages/wrap/tests/transforms.test.ts @@ -31,6 +31,7 @@ import { RenameInputObjectFields, MapLeafValues, TransformEnumValues, + FilterFieldDirectives, } from '@graphql-tools/wrap'; import { @@ -1639,3 +1640,51 @@ describe('TransformEnumValues', () => { expect(result.errors).toBeUndefined(); }); }); + +describe('FilterFieldDirectives', () => { + test('removes fields with unqualified 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 FilterFieldDirectives((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); + }); + + test('clears deprecations', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + type Query { + alpha:String @deprecated(reason: "keep") + bravo:String @deprecated(reason: "remove") + } + ` + }); + + const transformedSchema = wrapSchema(schema, [ + new FilterFieldDirectives((dirName: string, dirValue: any) => dirName === 'deprecated' && dirValue.reason === 'keep') + ]); + + const fields = transformedSchema.getType('Query').getFields(); + expect(fields.alpha.astNode.directives.length).toEqual(1); + expect(fields.alpha.deprecationReason).toEqual('keep'); + expect(fields.bravo.astNode.directives.length).toEqual(0); + expect(fields.bravo.deprecationReason).toBeUndefined(); + }); +}); From 4c366a27693356a394ffa4f016b5c5b4b800f57d Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Fri, 21 Aug 2020 23:23:40 -0400 Subject: [PATCH 22/28] more tests for valueMatchesCriteria. --- .../utils/tests/valueMatchesCriteria.test.ts | 52 +++++++++++++++++++ packages/utils/tests/valueMatchesCriteria.ts | 19 ------- 2 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 packages/utils/tests/valueMatchesCriteria.test.ts delete mode 100644 packages/utils/tests/valueMatchesCriteria.ts diff --git a/packages/utils/tests/valueMatchesCriteria.test.ts b/packages/utils/tests/valueMatchesCriteria.test.ts new file mode 100644 index 00000000000..3cc2bec7873 --- /dev/null +++ b/packages/utils/tests/valueMatchesCriteria.test.ts @@ -0,0 +1,52 @@ +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); + }); +}); diff --git a/packages/utils/tests/valueMatchesCriteria.ts b/packages/utils/tests/valueMatchesCriteria.ts deleted file mode 100644 index 2a85927052c..00000000000 --- a/packages/utils/tests/valueMatchesCriteria.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { valueMatchesCriteria } from '../src/index'; - -describe('valueMatchesCriteria', () => { - test('matches directive value object', () => { - 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, { this: 'reason' })).toBe(false); - expect(valueMatchesCriteria(dirValue, { reason: 'this' })).toBe(false); - }); - - test('matches empty arguments', () => { - expect(valueMatchesCriteria({}, {})).toBe(true); - }); -}); From 2ff6fa961f5084f81f2bfe75db58c1e6f44a043a Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Fri, 21 Aug 2020 23:36:16 -0400 Subject: [PATCH 23/28] cleanup stitching package tests. --- .../stitch/tests/transformDirectives.test.ts | 59 --------------- packages/stitch/tests/transforms.test.ts | 73 +++++++++++++++++++ 2 files changed, 73 insertions(+), 59 deletions(-) delete mode 100644 packages/stitch/tests/transformDirectives.test.ts diff --git a/packages/stitch/tests/transformDirectives.test.ts b/packages/stitch/tests/transformDirectives.test.ts deleted file mode 100644 index 07bcf3fe614..00000000000 --- a/packages/stitch/tests/transformDirectives.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { makeExecutableSchema } from '@graphql-tools/schema'; -import { stitchSchemas } from '../src/stitchSchemas'; -import { RemoveFieldDirectives, RemoveFieldsWithDirective } from '../src/index'; - -describe('transform deprecations', () => { - test('removes directives with arguments, includes deprecations', async () => { - const listingsSchema = makeExecutableSchema({ - typeDefs: ` - type Listing { - id: ID! - description: String! - price: Float! - sellerId: ID! @deprecated(reason: "stitching use only") - buyerId: ID @deprecated(reason: "stitching use only") - } - ` - }); - - const usersSchema = makeExecutableSchema({ - typeDefs: ` - type User { - id: ID! - email: String! @deprecated(reason: "other deprecation") - } - type Listing { - seller: User! @deprecated(reason: "gateway access only") - buyer: User @deprecated(reason: "gateway access only") - } - ` - }); - - const gatewaySchema = stitchSchemas({ - subschemas: [ - { - schema: listingsSchema, - transforms: [new RemoveFieldsWithDirective('deprecated', { reason: 'stitching use only' })] - }, - { - schema: usersSchema, - transforms: [new RemoveFieldDirectives('deprecated', { reason: 'gateway access only' })] - }, - ], - mergeTypes: true - }); - - expect(listingsSchema.getType('Listing').getFields().sellerId.deprecationReason).toBe('stitching use only'); - expect(gatewaySchema.getType('Listing').getFields().sellerId).toBe(undefined); - - expect(usersSchema.getType('Listing').getFields().seller.deprecationReason).toBe('gateway access only'); - expect(usersSchema.getType('Listing').getFields().seller.astNode.directives.length).toEqual(1); - expect(gatewaySchema.getType('Listing').getFields().seller.deprecationReason).toBe(undefined); - expect(gatewaySchema.getType('Listing').getFields().seller.astNode.directives.length).toEqual(0); - - expect(usersSchema.getType('User').getFields().email.deprecationReason).toBe('other deprecation'); - expect(usersSchema.getType('User').getFields().email.astNode.directives.length).toEqual(1); - expect(gatewaySchema.getType('User').getFields().email.deprecationReason).toBe('other deprecation'); - expect(gatewaySchema.getType('User').getFields().email.astNode.directives.length).toEqual(1); - }); -}); diff --git a/packages/stitch/tests/transforms.test.ts b/packages/stitch/tests/transforms.test.ts index 8e150dae783..1da63bc25ec 100644 --- a/packages/stitch/tests/transforms.test.ts +++ b/packages/stitch/tests/transforms.test.ts @@ -12,6 +12,12 @@ import { stitchSchemas } from '../src/stitchSchemas'; import { propertySchema } from './fixtures/schemas'; +import { + RemoveFieldDirectives, + RemoveFieldsWithDirective +} from '../src/index'; + + describe('rename root type', () => { test('works with stitchSchemas', async () => { let schemaWithCustomRootTypeNames = makeExecutableSchema({ @@ -128,3 +134,70 @@ describe('filter fields', () => { assertValidSchema(stitchedSchema); }); }); + +describe('RemoveFieldsWithDirective', () => { + test('works', async () => { + const listingsSchema = makeExecutableSchema({ + typeDefs: ` + type Listing { + id: ID! + description: String! + price: Float! @deprecated(reason: "other deprecation") + sellerId: ID! @deprecated(reason: "stitching use only") + buyerId: ID @deprecated(reason: "stitching use only") + } + ` + }); + + const stitchedSchema = stitchSchemas({ + subschemas: [ + { + schema: listingsSchema, + transforms: [new RemoveFieldsWithDirective('deprecated', { reason: 'stitching use only' })] + } + ], + }); + + expect(listingsSchema.getType('Listing').getFields().price.deprecationReason).toBe('other deprecation'); + expect(listingsSchema.getType('Listing').getFields().sellerId.deprecationReason).toBe('stitching use only'); + + expect(stitchedSchema.getType('Listing').getFields().price.deprecationReason).toBe('other deprecation'); + expect(stitchedSchema.getType('Listing').getFields().sellerId).toBeUndefined(); + }); +}); + +describe('RemoveFieldDirectives', () => { + test('works', async () => { + const usersSchema = makeExecutableSchema({ + typeDefs: ` + type User { + id: ID! + email: String! @deprecated(reason: "other deprecation") + } + type Listing { + seller: User! @deprecated(reason: "gateway access only") + buyer: User @deprecated(reason: "gateway access only") + } + ` + }); + + const stitchedSchema = stitchSchemas({ + subschemas: [ + { + schema: usersSchema, + transforms: [new RemoveFieldDirectives('deprecated', { reason: 'gateway access only' })] + }, + ], + }); + + expect(usersSchema.getType('Listing').getFields().seller.deprecationReason).toBe('gateway access only'); + expect(usersSchema.getType('Listing').getFields().seller.astNode.directives.length).toEqual(1); + expect(stitchedSchema.getType('Listing').getFields().seller.deprecationReason).toBeUndefined(); + expect(stitchedSchema.getType('Listing').getFields().seller.astNode.directives.length).toEqual(0); + + expect(usersSchema.getType('User').getFields().email.deprecationReason).toBe('other deprecation'); + expect(usersSchema.getType('User').getFields().email.astNode.directives.length).toEqual(1); + expect(stitchedSchema.getType('User').getFields().email.deprecationReason).toBe('other deprecation'); + expect(stitchedSchema.getType('User').getFields().email.astNode.directives.length).toEqual(1); + }); +}); \ No newline at end of file From 03564a4b926cb703083d65bc6f4f4684d1c083b5 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sun, 23 Aug 2020 13:39:56 -0400 Subject: [PATCH 24/28] break apart deprecations from directive filters. --- packages/stitch/src/.DS_Store | Bin 0 -> 6148 bytes packages/stitch/src/index.ts | 2 - packages/stitch/tests/transforms.test.ts | 73 ------------------ packages/utils/src/valueMatchesCriteria.ts | 2 + .../utils/tests/valueMatchesCriteria.test.ts | 7 ++ .../src/transforms/FilterFieldDirectives.ts | 4 - .../src/transforms/RemoveDeprecatedFields.ts | 25 ++++++ .../wrap/src/transforms/RemoveDeprecations.ts | 33 ++++++++ .../src/transforms/RemoveFieldDirectives.ts | 7 +- .../transforms/RemoveFieldsWithDirective.ts | 7 +- packages/wrap/src/transforms/index.ts | 6 +- .../transformFilterFieldDirectives.test.ts | 29 +++++++ .../transformRemoveDeprecatedFields.test.ts | 34 ++++++++ .../tests/transformRemoveDeprecations.test.ts | 38 +++++++++ .../transformRemoveFieldDirectives.test.ts | 66 ++++++++++++++++ ...transformRemoveFieldsWithDirective.test.ts | 67 ++++++++++++++++ packages/wrap/tests/transforms.test.ts | 49 ------------ website/docs/stitch-type-merging.md | 8 +- 18 files changed, 320 insertions(+), 137 deletions(-) create mode 100644 packages/stitch/src/.DS_Store create mode 100644 packages/wrap/src/transforms/RemoveDeprecatedFields.ts create mode 100644 packages/wrap/src/transforms/RemoveDeprecations.ts rename packages/{stitch => wrap}/src/transforms/RemoveFieldDirectives.ts (67%) rename packages/{stitch => wrap}/src/transforms/RemoveFieldsWithDirective.ts (79%) create mode 100644 packages/wrap/tests/transformFilterFieldDirectives.test.ts create mode 100644 packages/wrap/tests/transformRemoveDeprecatedFields.test.ts create mode 100644 packages/wrap/tests/transformRemoveDeprecations.test.ts create mode 100644 packages/wrap/tests/transformRemoveFieldDirectives.test.ts create mode 100644 packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts diff --git a/packages/stitch/src/.DS_Store b/packages/stitch/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..55fd88998090877c9753006396b116046ba6b288 GIT binary patch literal 6148 zcmeHKyG{c^3>-s>2%3~B_ZRqsRg`=|egGsC8n{T|QD2qs%BL}Y2oaqSX=u<`vS-)p z+0#vNJ_E4T`{Nz305GRJ;^<*&e(pZ8tI8OW&O2VQ!yX5`aCMnE_Xcm|JYe|F|JWXe z=l$@McVBjys-%DvkOERb3P^!p74Y6mo8KlXN&zV#1-=yU??a { test('works with stitchSchemas', async () => { let schemaWithCustomRootTypeNames = makeExecutableSchema({ @@ -134,70 +128,3 @@ describe('filter fields', () => { assertValidSchema(stitchedSchema); }); }); - -describe('RemoveFieldsWithDirective', () => { - test('works', async () => { - const listingsSchema = makeExecutableSchema({ - typeDefs: ` - type Listing { - id: ID! - description: String! - price: Float! @deprecated(reason: "other deprecation") - sellerId: ID! @deprecated(reason: "stitching use only") - buyerId: ID @deprecated(reason: "stitching use only") - } - ` - }); - - const stitchedSchema = stitchSchemas({ - subschemas: [ - { - schema: listingsSchema, - transforms: [new RemoveFieldsWithDirective('deprecated', { reason: 'stitching use only' })] - } - ], - }); - - expect(listingsSchema.getType('Listing').getFields().price.deprecationReason).toBe('other deprecation'); - expect(listingsSchema.getType('Listing').getFields().sellerId.deprecationReason).toBe('stitching use only'); - - expect(stitchedSchema.getType('Listing').getFields().price.deprecationReason).toBe('other deprecation'); - expect(stitchedSchema.getType('Listing').getFields().sellerId).toBeUndefined(); - }); -}); - -describe('RemoveFieldDirectives', () => { - test('works', async () => { - const usersSchema = makeExecutableSchema({ - typeDefs: ` - type User { - id: ID! - email: String! @deprecated(reason: "other deprecation") - } - type Listing { - seller: User! @deprecated(reason: "gateway access only") - buyer: User @deprecated(reason: "gateway access only") - } - ` - }); - - const stitchedSchema = stitchSchemas({ - subschemas: [ - { - schema: usersSchema, - transforms: [new RemoveFieldDirectives('deprecated', { reason: 'gateway access only' })] - }, - ], - }); - - expect(usersSchema.getType('Listing').getFields().seller.deprecationReason).toBe('gateway access only'); - expect(usersSchema.getType('Listing').getFields().seller.astNode.directives.length).toEqual(1); - expect(stitchedSchema.getType('Listing').getFields().seller.deprecationReason).toBeUndefined(); - expect(stitchedSchema.getType('Listing').getFields().seller.astNode.directives.length).toEqual(0); - - expect(usersSchema.getType('User').getFields().email.deprecationReason).toBe('other deprecation'); - expect(usersSchema.getType('User').getFields().email.astNode.directives.length).toEqual(1); - expect(stitchedSchema.getType('User').getFields().email.deprecationReason).toBe('other deprecation'); - expect(stitchedSchema.getType('User').getFields().email.astNode.directives.length).toEqual(1); - }); -}); \ No newline at end of file diff --git a/packages/utils/src/valueMatchesCriteria.ts b/packages/utils/src/valueMatchesCriteria.ts index 321ba91e1a8..1da37d7760d 100644 --- a/packages/utils/src/valueMatchesCriteria.ts +++ b/packages/utils/src/valueMatchesCriteria.ts @@ -9,6 +9,8 @@ export function valueMatchesCriteria(value: any, criteria: any): boolean { criteria && Object.keys(criteria).every(propertyName => valueMatchesCriteria(value[propertyName], criteria[propertyName])) ); + } else if (criteria instanceof RegExp) { + return criteria.test(value); } return value === criteria; diff --git a/packages/utils/tests/valueMatchesCriteria.test.ts b/packages/utils/tests/valueMatchesCriteria.test.ts index 3cc2bec7873..cd6c69cb8e5 100644 --- a/packages/utils/tests/valueMatchesCriteria.test.ts +++ b/packages/utils/tests/valueMatchesCriteria.test.ts @@ -49,4 +49,11 @@ describe('valueMatchesCriteria', () => { 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); + }); }); diff --git a/packages/wrap/src/transforms/FilterFieldDirectives.ts b/packages/wrap/src/transforms/FilterFieldDirectives.ts index 3b9587a584b..e78433915ba 100644 --- a/packages/wrap/src/transforms/FilterFieldDirectives.ts +++ b/packages/wrap/src/transforms/FilterFieldDirectives.ts @@ -26,10 +26,6 @@ export default class FilterFieldDirectives implements Transform { directives: keepDirectives, }, }; - - if (fieldConfig.deprecationReason && !keepDirectives.some(dir => dir.name.value === 'deprecated')) { - delete fieldConfig.deprecationReason; - } return fieldConfig; } } diff --git a/packages/wrap/src/transforms/RemoveDeprecatedFields.ts b/packages/wrap/src/transforms/RemoveDeprecatedFields.ts new file mode 100644 index 00000000000..a8d154fcd1f --- /dev/null +++ b/packages/wrap/src/transforms/RemoveDeprecatedFields.ts @@ -0,0 +1,25 @@ +import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; +import { Transform } from '@graphql-tools/utils'; +import { FilterObjectFields } from '@graphql-tools/wrap'; + +export default class RemoveDeprecatedFields implements Transform { + private readonly transformer: FilterObjectFields; + + constructor(reason: string | RegExp) { + this.transformer = new FilterObjectFields( + (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { + if (fieldConfig.deprecationReason) { + return !( + (reason instanceof RegExp && reason.test(fieldConfig.deprecationReason)) || + reason === fieldConfig.deprecationReason + ); + } + return true; + } + ); + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return this.transformer.transformSchema(originalSchema); + } +} diff --git a/packages/wrap/src/transforms/RemoveDeprecations.ts b/packages/wrap/src/transforms/RemoveDeprecations.ts new file mode 100644 index 00000000000..552bcfdf9b4 --- /dev/null +++ b/packages/wrap/src/transforms/RemoveDeprecations.ts @@ -0,0 +1,33 @@ +import { GraphQLSchema } from 'graphql'; +import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; +import { FilterFieldDirectives, TransformObjectFields } from '@graphql-tools/wrap'; + +export default class RemoveDeprecations implements Transform { + private readonly removeDirectives: FilterFieldDirectives; + private readonly removeDeprecations: TransformObjectFields; + + constructor(reason: string | RegExp) { + const args = { reason }; + this.removeDirectives = new FilterFieldDirectives((dirName: string, dirValue: any) => { + return !(dirName === 'deprecated' && valueMatchesCriteria(dirValue, args)); + }); + this.removeDeprecations = new TransformObjectFields( + (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { + if (fieldConfig.deprecationReason) { + if ( + (reason instanceof RegExp && reason.test(fieldConfig.deprecationReason)) || + reason === fieldConfig.deprecationReason + ) { + fieldConfig = { ...fieldConfig }; + delete fieldConfig.deprecationReason; + } + } + return fieldConfig; + } + ); + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return this.removeDeprecations.transformSchema(this.removeDirectives.transformSchema(originalSchema)); + } +} diff --git a/packages/stitch/src/transforms/RemoveFieldDirectives.ts b/packages/wrap/src/transforms/RemoveFieldDirectives.ts similarity index 67% rename from packages/stitch/src/transforms/RemoveFieldDirectives.ts rename to packages/wrap/src/transforms/RemoveFieldDirectives.ts index 9929397eb53..5f3d82776c1 100644 --- a/packages/stitch/src/transforms/RemoveFieldDirectives.ts +++ b/packages/wrap/src/transforms/RemoveFieldDirectives.ts @@ -5,9 +5,12 @@ import { FilterFieldDirectives } from '@graphql-tools/wrap'; export default class RemoveFieldDirectives implements Transform { private readonly transformer: FilterFieldDirectives; - constructor(directiveName: string, args: Record = {}) { + constructor(directiveName: string | RegExp, args: Record = {}) { this.transformer = new FilterFieldDirectives((dirName: string, dirValue: any) => { - return !(directiveName === dirName && valueMatchesCriteria(dirValue, args)); + return !( + ((directiveName instanceof RegExp && directiveName.test(dirName)) || directiveName === dirName) && + valueMatchesCriteria(dirValue, args) + ); }); } diff --git a/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts b/packages/wrap/src/transforms/RemoveFieldsWithDirective.ts similarity index 79% rename from packages/stitch/src/transforms/RemoveFieldsWithDirective.ts rename to packages/wrap/src/transforms/RemoveFieldsWithDirective.ts index 772f526dbf0..f3f79f76022 100644 --- a/packages/stitch/src/transforms/RemoveFieldsWithDirective.ts +++ b/packages/wrap/src/transforms/RemoveFieldsWithDirective.ts @@ -3,10 +3,10 @@ import { Transform, getDirectives, valueMatchesCriteria } from '@graphql-tools/u import { FilterObjectFields } from '@graphql-tools/wrap'; export default class RemoveFieldsWithDirective implements Transform { - private readonly directiveName: string; + private readonly directiveName: string | RegExp; private readonly args: Record; - constructor(directiveName: string, args: Record = {}) { + constructor(directiveName: string | RegExp, args: Record = {}) { this.directiveName = directiveName; this.args = args; } @@ -17,7 +17,8 @@ export default class RemoveFieldsWithDirective implements Transform { const valueMap = getDirectives(originalSchema, fieldConfig); return !Object.keys(valueMap).some( directiveName => - directiveName === this.directiveName && + ((this.directiveName instanceof RegExp && this.directiveName.test(directiveName)) || + this.directiveName === directiveName) && ((Array.isArray(valueMap[directiveName]) && valueMap[directiveName].some((value: any) => valueMatchesCriteria(value, this.args))) || valueMatchesCriteria(valueMap[directiveName], this.args)) diff --git a/packages/wrap/src/transforms/index.ts b/packages/wrap/src/transforms/index.ts index 853d094d2fb..215692903f0 100644 --- a/packages/wrap/src/transforms/index.ts +++ b/packages/wrap/src/transforms/index.ts @@ -14,10 +14,14 @@ export { default as FilterInterfaceFields } from './FilterInterfaceFields'; export { default as TransformInputObjectFields } from './TransformInputObjectFields'; export { default as RenameInputObjectFields } from './RenameInputObjectFields'; export { default as FilterInputObjectFields } from './FilterInputObjectFields'; -export { default as FilterFieldDirectives } from './FilterFieldDirectives'; export { default as MapLeafValues } from './MapLeafValues'; export { default as TransformEnumValues } from './TransformEnumValues'; export { default as TransformQuery } from './TransformQuery'; +export { default as FilterFieldDirectives } from './FilterFieldDirectives'; +export { default as RemoveFieldDirectives } from './RemoveFieldDirectives'; +export { default as RemoveFieldsWithDirective } from './RemoveFieldsWithDirective'; +export { default as RemoveDeprecatedFields } from './RemoveDeprecatedFields'; +export { default as RemoveDeprecations } from './RemoveDeprecations'; export { default as ExtendSchema } from './ExtendSchema'; export { default as PruneSchema } from './PruneSchema'; diff --git a/packages/wrap/tests/transformFilterFieldDirectives.test.ts b/packages/wrap/tests/transformFilterFieldDirectives.test.ts new file mode 100644 index 00000000000..8d807b2a187 --- /dev/null +++ b/packages/wrap/tests/transformFilterFieldDirectives.test.ts @@ -0,0 +1,29 @@ +import { wrapSchema, FilterFieldDirectives } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('FilterFieldDirectives', () => { + test('removes fields with unqualified 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 FilterFieldDirectives((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); + }); +}); diff --git a/packages/wrap/tests/transformRemoveDeprecatedFields.test.ts b/packages/wrap/tests/transformRemoveDeprecatedFields.test.ts new file mode 100644 index 00000000000..c304d008e3d --- /dev/null +++ b/packages/wrap/tests/transformRemoveDeprecatedFields.test.ts @@ -0,0 +1,34 @@ +import { wrapSchema, RemoveDeprecatedFields } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('RemoveDeprecatedFields', () => { + const originalSchema = makeExecutableSchema({ + typeDefs: ` + type Test { + id: ID! + first: String! @deprecated(reason: "do not remove") + second: String! @deprecated(reason: "remove this") + } + ` + }); + + test('removes directives by name', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveDeprecatedFields('remove this') + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.first).toBeDefined(); + expect(fields.second).toBeUndefined(); + }); + + test('removes directives by name regex', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveDeprecatedFields(/remove/) + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.first).toBeUndefined(); + expect(fields.second).toBeUndefined(); + }); +}); diff --git a/packages/wrap/tests/transformRemoveDeprecations.test.ts b/packages/wrap/tests/transformRemoveDeprecations.test.ts new file mode 100644 index 00000000000..f5cc9ab0e76 --- /dev/null +++ b/packages/wrap/tests/transformRemoveDeprecations.test.ts @@ -0,0 +1,38 @@ +import { wrapSchema, RemoveDeprecations } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('RemoveDeprecations', () => { + const originalSchema = makeExecutableSchema({ + typeDefs: ` + type Test { + id: ID! + first: String! @deprecated(reason: "do not remove") + second: String! @deprecated(reason: "remove this") + } + ` + }); + + test('removes directives by name', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveDeprecations('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 directives by name regex', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveDeprecations(/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); + }); +}); diff --git a/packages/wrap/tests/transformRemoveFieldDirectives.test.ts b/packages/wrap/tests/transformRemoveFieldDirectives.test.ts new file mode 100644 index 00000000000..1c2edbe9ea2 --- /dev/null +++ b/packages/wrap/tests/transformRemoveFieldDirectives.test.ts @@ -0,0 +1,66 @@ +import { wrapSchema, RemoveFieldDirectives } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('RemoveFieldDirectives', () => { + 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 RemoveFieldDirectives('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 RemoveFieldDirectives(/^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 RemoveFieldDirectives(/.+/, { 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 RemoveFieldDirectives(/.+/, { 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); + }); +}); diff --git a/packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts b/packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts new file mode 100644 index 00000000000..b02796bacd0 --- /dev/null +++ b/packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts @@ -0,0 +1,67 @@ +import { wrapSchema, RemoveFieldsWithDirective } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('RemoveFieldsWithDirective', () => { + const originalSchema = makeExecutableSchema({ + typeDefs: ` + directive @alpha(arg: String) on FIELD_DEFINITION + directive @bravo(arg: String) on FIELD_DEFINITION + + type Test { + id: ID! + first: String! @alpha(arg: "do not remove") + second: String! @alpha(arg: "remove this") + third: String @alpha(arg: "remove this") + fourth: String @bravo(arg: "remove this") + } + ` + }); + + test('removes directives by name', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveFieldsWithDirective('alpha') + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.first).toBeUndefined(); + expect(fields.second).toBeUndefined(); + expect(fields.third).toBeUndefined(); + expect(fields.fourth).toBeDefined(); + }); + + test('removes directives by name regex', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveFieldsWithDirective(/^alp/) + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.first).toBeUndefined(); + expect(fields.second).toBeUndefined(); + expect(fields.third).toBeUndefined(); + expect(fields.fourth).toBeDefined(); + }); + + test('removes directives by argument', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveFieldsWithDirective(/.+/, { arg: 'remove this' }) + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.first).toBeDefined(); + expect(fields.second).toBeUndefined(); + expect(fields.third).toBeUndefined(); + expect(fields.third).toBeUndefined(); + }); + + test('removes directives by argument regex', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveFieldsWithDirective(/.+/, { arg: /remove/ }) + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.first).toBeUndefined(); + expect(fields.second).toBeUndefined(); + expect(fields.third).toBeUndefined(); + expect(fields.third).toBeUndefined(); + }); +}); diff --git a/packages/wrap/tests/transforms.test.ts b/packages/wrap/tests/transforms.test.ts index 64cbb38a4e4..6275804313e 100644 --- a/packages/wrap/tests/transforms.test.ts +++ b/packages/wrap/tests/transforms.test.ts @@ -31,7 +31,6 @@ import { RenameInputObjectFields, MapLeafValues, TransformEnumValues, - FilterFieldDirectives, } from '@graphql-tools/wrap'; import { @@ -1640,51 +1639,3 @@ describe('TransformEnumValues', () => { expect(result.errors).toBeUndefined(); }); }); - -describe('FilterFieldDirectives', () => { - test('removes fields with unqualified 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 FilterFieldDirectives((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); - }); - - test('clears deprecations', async () => { - const schema = makeExecutableSchema({ - typeDefs: ` - type Query { - alpha:String @deprecated(reason: "keep") - bravo:String @deprecated(reason: "remove") - } - ` - }); - - const transformedSchema = wrapSchema(schema, [ - new FilterFieldDirectives((dirName: string, dirValue: any) => dirName === 'deprecated' && dirValue.reason === 'keep') - ]); - - const fields = transformedSchema.getType('Query').getFields(); - expect(fields.alpha.astNode.directives.length).toEqual(1); - expect(fields.alpha.deprecationReason).toEqual('keep'); - expect(fields.bravo.astNode.directives.length).toEqual(0); - expect(fields.bravo.deprecationReason).toBeUndefined(); - }); -}); diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index 22e744fd450..1241a0c6df0 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -397,7 +397,7 @@ However, you may notice that both `buyerId` and `sellerId` keys are _always_ req One disadvantage of this pattern is that we end up with clutter—`buyerId`/`sellerId` are extra fields, and `buyer`/`seller` fields have gateway dependencies. To tidy things up, we can aggressively deprecate these fields in subschemas and then remove/normalize their behavior in the gateway using available transforms: ```js -import { RemoveFieldsWithDirective, RemoveFieldDirectives } from '@graphql-tools/stitch'; +import { RemoveDeprecatedFields, RemoveDeprecations } from '@graphql-tools/wrap'; const listingsSchema = makeExecutableSchema({ typeDefs: ` @@ -429,18 +429,20 @@ const gatewaySchema = stitchSchemas({ subschemas: [ { schema: listingsSchema, - transforms: [new RemoveFieldsWithDirective('deprecated', { reason: 'stitching use only' })], + transforms: [new RemoveDeprecatedFields('stitching use only')], merge: { ... } }, { schema: usersSchema, - transforms: [new RemoveFieldDirectives('deprecated', { reason: 'gateway access only' })], + transforms: [new RemoveDeprecations('gateway access only')], merge: { ... } }, ], }); ``` +Cleanup can also be performed on custom directives using `RemoveFieldsWithDirective` and/or `RemoveFieldDirectives` transforms. + ### Federation services If you're familiar with [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/), then you may notice that the above pattern of injected keys looks familiar... You're right, it's very similar to the `_entities` service design of the [Federation schema specification](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/). From 4d2b3d994f2f7b9c2ed12204022ee33d285360e1 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sun, 23 Aug 2020 14:01:54 -0400 Subject: [PATCH 25/28] cleanup naming and fix typescript error. --- ...oveDeprecations.ts => RemoveFieldDeprecations.ts} | 4 ++-- ...catedFields.ts => RemoveFieldsWithDeprecation.ts} | 2 +- packages/wrap/src/transforms/index.ts | 4 ++-- .../tests/transformFilterFieldDirectives.test.ts | 2 +- .../tests/transformRemoveDeprecatedFields.test.ts | 12 ++++++------ ...t.ts => transformRemoveFieldDeprecations.test.ts} | 12 ++++++------ .../tests/transformRemoveFieldsWithDirective.test.ts | 8 ++++---- website/docs/stitch-type-merging.md | 6 +++--- 8 files changed, 25 insertions(+), 25 deletions(-) rename packages/wrap/src/transforms/{RemoveDeprecations.ts => RemoveFieldDeprecations.ts} (90%) rename packages/wrap/src/transforms/{RemoveDeprecatedFields.ts => RemoveFieldsWithDeprecation.ts} (91%) rename packages/wrap/tests/{transformRemoveDeprecations.test.ts => transformRemoveFieldDeprecations.test.ts} (76%) diff --git a/packages/wrap/src/transforms/RemoveDeprecations.ts b/packages/wrap/src/transforms/RemoveFieldDeprecations.ts similarity index 90% rename from packages/wrap/src/transforms/RemoveDeprecations.ts rename to packages/wrap/src/transforms/RemoveFieldDeprecations.ts index 552bcfdf9b4..b8fe7d1f5cd 100644 --- a/packages/wrap/src/transforms/RemoveDeprecations.ts +++ b/packages/wrap/src/transforms/RemoveFieldDeprecations.ts @@ -1,8 +1,8 @@ -import { GraphQLSchema } from 'graphql'; +import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; import { FilterFieldDirectives, TransformObjectFields } from '@graphql-tools/wrap'; -export default class RemoveDeprecations implements Transform { +export default class RemoveFieldDeprecations implements Transform { private readonly removeDirectives: FilterFieldDirectives; private readonly removeDeprecations: TransformObjectFields; diff --git a/packages/wrap/src/transforms/RemoveDeprecatedFields.ts b/packages/wrap/src/transforms/RemoveFieldsWithDeprecation.ts similarity index 91% rename from packages/wrap/src/transforms/RemoveDeprecatedFields.ts rename to packages/wrap/src/transforms/RemoveFieldsWithDeprecation.ts index a8d154fcd1f..b5f5593edf5 100644 --- a/packages/wrap/src/transforms/RemoveDeprecatedFields.ts +++ b/packages/wrap/src/transforms/RemoveFieldsWithDeprecation.ts @@ -2,7 +2,7 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; import { Transform } from '@graphql-tools/utils'; import { FilterObjectFields } from '@graphql-tools/wrap'; -export default class RemoveDeprecatedFields implements Transform { +export default class RemoveFieldsWithDeprecation implements Transform { private readonly transformer: FilterObjectFields; constructor(reason: string | RegExp) { diff --git a/packages/wrap/src/transforms/index.ts b/packages/wrap/src/transforms/index.ts index 215692903f0..70ca2126c48 100644 --- a/packages/wrap/src/transforms/index.ts +++ b/packages/wrap/src/transforms/index.ts @@ -20,8 +20,8 @@ export { default as TransformQuery } from './TransformQuery'; export { default as FilterFieldDirectives } from './FilterFieldDirectives'; export { default as RemoveFieldDirectives } from './RemoveFieldDirectives'; export { default as RemoveFieldsWithDirective } from './RemoveFieldsWithDirective'; -export { default as RemoveDeprecatedFields } from './RemoveDeprecatedFields'; -export { default as RemoveDeprecations } from './RemoveDeprecations'; +export { default as RemoveFieldDeprecations } from './RemoveFieldDeprecations'; +export { default as RemoveFieldsWithDeprecation } from './RemoveFieldsWithDeprecation'; export { default as ExtendSchema } from './ExtendSchema'; export { default as PruneSchema } from './PruneSchema'; diff --git a/packages/wrap/tests/transformFilterFieldDirectives.test.ts b/packages/wrap/tests/transformFilterFieldDirectives.test.ts index 8d807b2a187..3736146c297 100644 --- a/packages/wrap/tests/transformFilterFieldDirectives.test.ts +++ b/packages/wrap/tests/transformFilterFieldDirectives.test.ts @@ -2,7 +2,7 @@ import { wrapSchema, FilterFieldDirectives } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; describe('FilterFieldDirectives', () => { - test('removes fields with unqualified directives', async () => { + test('removes unmatched field directives', async () => { const schema = makeExecutableSchema({ typeDefs: ` directive @remove on FIELD_DEFINITION diff --git a/packages/wrap/tests/transformRemoveDeprecatedFields.test.ts b/packages/wrap/tests/transformRemoveDeprecatedFields.test.ts index c304d008e3d..1294623b4a2 100644 --- a/packages/wrap/tests/transformRemoveDeprecatedFields.test.ts +++ b/packages/wrap/tests/transformRemoveDeprecatedFields.test.ts @@ -1,7 +1,7 @@ -import { wrapSchema, RemoveDeprecatedFields } from '@graphql-tools/wrap'; +import { wrapSchema, RemoveFieldsWithDeprecation } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; -describe('RemoveDeprecatedFields', () => { +describe('RemoveFieldsWithDeprecation', () => { const originalSchema = makeExecutableSchema({ typeDefs: ` type Test { @@ -12,9 +12,9 @@ describe('RemoveDeprecatedFields', () => { ` }); - test('removes directives by name', async () => { + test('removes deprecated fields by reason', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveDeprecatedFields('remove this') + new RemoveFieldsWithDeprecation('remove this') ]); const fields = transformedSchema.getType('Test').getFields(); @@ -22,9 +22,9 @@ describe('RemoveDeprecatedFields', () => { expect(fields.second).toBeUndefined(); }); - test('removes directives by name regex', async () => { + test('removes deprecated fields by reason regex', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveDeprecatedFields(/remove/) + new RemoveFieldsWithDeprecation(/remove/) ]); const fields = transformedSchema.getType('Test').getFields(); diff --git a/packages/wrap/tests/transformRemoveDeprecations.test.ts b/packages/wrap/tests/transformRemoveFieldDeprecations.test.ts similarity index 76% rename from packages/wrap/tests/transformRemoveDeprecations.test.ts rename to packages/wrap/tests/transformRemoveFieldDeprecations.test.ts index f5cc9ab0e76..351536b85c2 100644 --- a/packages/wrap/tests/transformRemoveDeprecations.test.ts +++ b/packages/wrap/tests/transformRemoveFieldDeprecations.test.ts @@ -1,7 +1,7 @@ -import { wrapSchema, RemoveDeprecations } from '@graphql-tools/wrap'; +import { wrapSchema, RemoveFieldDeprecations } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; -describe('RemoveDeprecations', () => { +describe('RemoveFieldDeprecations', () => { const originalSchema = makeExecutableSchema({ typeDefs: ` type Test { @@ -12,9 +12,9 @@ describe('RemoveDeprecations', () => { ` }); - test('removes directives by name', async () => { + test('removes deprecations by reason', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveDeprecations('remove this') + new RemoveFieldDeprecations('remove this') ]); const fields = transformedSchema.getType('Test').getFields(); @@ -24,9 +24,9 @@ describe('RemoveDeprecations', () => { expect(fields.second.astNode.directives.length).toEqual(0); }); - test('removes directives by name regex', async () => { + test('removes deprecations by reason regex', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveDeprecations(/remove/) + new RemoveFieldDeprecations(/remove/) ]); const fields = transformedSchema.getType('Test').getFields(); diff --git a/packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts b/packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts index b02796bacd0..bfaebb6df23 100644 --- a/packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts +++ b/packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts @@ -17,7 +17,7 @@ describe('RemoveFieldsWithDirective', () => { ` }); - test('removes directives by name', async () => { + test('removes directive fields by name', async () => { const transformedSchema = wrapSchema(originalSchema, [ new RemoveFieldsWithDirective('alpha') ]); @@ -29,7 +29,7 @@ describe('RemoveFieldsWithDirective', () => { expect(fields.fourth).toBeDefined(); }); - test('removes directives by name regex', async () => { + test('removes directive fields by name regex', async () => { const transformedSchema = wrapSchema(originalSchema, [ new RemoveFieldsWithDirective(/^alp/) ]); @@ -41,7 +41,7 @@ describe('RemoveFieldsWithDirective', () => { expect(fields.fourth).toBeDefined(); }); - test('removes directives by argument', async () => { + test('removes directive fields by argument', async () => { const transformedSchema = wrapSchema(originalSchema, [ new RemoveFieldsWithDirective(/.+/, { arg: 'remove this' }) ]); @@ -53,7 +53,7 @@ describe('RemoveFieldsWithDirective', () => { expect(fields.third).toBeUndefined(); }); - test('removes directives by argument regex', async () => { + test('removes directive fields by argument regex', async () => { const transformedSchema = wrapSchema(originalSchema, [ new RemoveFieldsWithDirective(/.+/, { arg: /remove/ }) ]); diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index 1241a0c6df0..06574f8c9ac 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -397,7 +397,7 @@ However, you may notice that both `buyerId` and `sellerId` keys are _always_ req One disadvantage of this pattern is that we end up with clutter—`buyerId`/`sellerId` are extra fields, and `buyer`/`seller` fields have gateway dependencies. To tidy things up, we can aggressively deprecate these fields in subschemas and then remove/normalize their behavior in the gateway using available transforms: ```js -import { RemoveDeprecatedFields, RemoveDeprecations } from '@graphql-tools/wrap'; +import { RemoveFieldsWithDeprecation, RemoveFieldDeprecations } from '@graphql-tools/wrap'; const listingsSchema = makeExecutableSchema({ typeDefs: ` @@ -429,12 +429,12 @@ const gatewaySchema = stitchSchemas({ subschemas: [ { schema: listingsSchema, - transforms: [new RemoveDeprecatedFields('stitching use only')], + transforms: [new RemoveFieldsWithDeprecation('stitching use only')], merge: { ... } }, { schema: usersSchema, - transforms: [new RemoveDeprecations('gateway access only')], + transforms: [new RemoveFieldDeprecations('gateway access only')], merge: { ... } }, ], From 57b7d67f0e409e6791477e4494d0a0fccf1a8b21 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sun, 23 Aug 2020 14:09:50 -0400 Subject: [PATCH 26/28] [deploy_website] remove nested ifs. --- .../src/transforms/RemoveFieldDeprecations.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/wrap/src/transforms/RemoveFieldDeprecations.ts b/packages/wrap/src/transforms/RemoveFieldDeprecations.ts index b8fe7d1f5cd..220983909a3 100644 --- a/packages/wrap/src/transforms/RemoveFieldDeprecations.ts +++ b/packages/wrap/src/transforms/RemoveFieldDeprecations.ts @@ -13,14 +13,13 @@ export default class RemoveFieldDeprecations implements Transform { }); this.removeDeprecations = new TransformObjectFields( (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { - if (fieldConfig.deprecationReason) { - if ( - (reason instanceof RegExp && reason.test(fieldConfig.deprecationReason)) || - reason === fieldConfig.deprecationReason - ) { - fieldConfig = { ...fieldConfig }; - delete fieldConfig.deprecationReason; - } + if ( + fieldConfig.deprecationReason && + ((reason instanceof RegExp && reason.test(fieldConfig.deprecationReason)) || + reason === fieldConfig.deprecationReason) + ) { + fieldConfig = { ...fieldConfig }; + delete fieldConfig.deprecationReason; } return fieldConfig; } From ce730da723b2c26fa3d1ad9e60f90bce939d4402 Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sun, 23 Aug 2020 14:16:51 -0400 Subject: [PATCH 27/28] eat more dogfood. --- packages/wrap/src/transforms/RemoveFieldDeprecations.ts | 6 +----- packages/wrap/src/transforms/RemoveFieldDirectives.ts | 5 +---- .../wrap/src/transforms/RemoveFieldsWithDeprecation.ts | 7 ++----- packages/wrap/src/transforms/RemoveFieldsWithDirective.ts | 3 +-- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/wrap/src/transforms/RemoveFieldDeprecations.ts b/packages/wrap/src/transforms/RemoveFieldDeprecations.ts index 220983909a3..7f5bb01b2ce 100644 --- a/packages/wrap/src/transforms/RemoveFieldDeprecations.ts +++ b/packages/wrap/src/transforms/RemoveFieldDeprecations.ts @@ -13,11 +13,7 @@ export default class RemoveFieldDeprecations implements Transform { }); this.removeDeprecations = new TransformObjectFields( (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { - if ( - fieldConfig.deprecationReason && - ((reason instanceof RegExp && reason.test(fieldConfig.deprecationReason)) || - reason === fieldConfig.deprecationReason) - ) { + if (fieldConfig.deprecationReason && valueMatchesCriteria(fieldConfig.deprecationReason, reason)) { fieldConfig = { ...fieldConfig }; delete fieldConfig.deprecationReason; } diff --git a/packages/wrap/src/transforms/RemoveFieldDirectives.ts b/packages/wrap/src/transforms/RemoveFieldDirectives.ts index 5f3d82776c1..9db89290a86 100644 --- a/packages/wrap/src/transforms/RemoveFieldDirectives.ts +++ b/packages/wrap/src/transforms/RemoveFieldDirectives.ts @@ -7,10 +7,7 @@ export default class RemoveFieldDirectives implements Transform { constructor(directiveName: string | RegExp, args: Record = {}) { this.transformer = new FilterFieldDirectives((dirName: string, dirValue: any) => { - return !( - ((directiveName instanceof RegExp && directiveName.test(dirName)) || directiveName === dirName) && - valueMatchesCriteria(dirValue, args) - ); + return !(valueMatchesCriteria(dirName, directiveName) && valueMatchesCriteria(dirValue, args)); }); } diff --git a/packages/wrap/src/transforms/RemoveFieldsWithDeprecation.ts b/packages/wrap/src/transforms/RemoveFieldsWithDeprecation.ts index b5f5593edf5..34cce8c442b 100644 --- a/packages/wrap/src/transforms/RemoveFieldsWithDeprecation.ts +++ b/packages/wrap/src/transforms/RemoveFieldsWithDeprecation.ts @@ -1,5 +1,5 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; -import { Transform } from '@graphql-tools/utils'; +import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; import { FilterObjectFields } from '@graphql-tools/wrap'; export default class RemoveFieldsWithDeprecation implements Transform { @@ -9,10 +9,7 @@ export default class RemoveFieldsWithDeprecation implements Transform { this.transformer = new FilterObjectFields( (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { if (fieldConfig.deprecationReason) { - return !( - (reason instanceof RegExp && reason.test(fieldConfig.deprecationReason)) || - reason === fieldConfig.deprecationReason - ); + return !valueMatchesCriteria(fieldConfig.deprecationReason, reason); } return true; } diff --git a/packages/wrap/src/transforms/RemoveFieldsWithDirective.ts b/packages/wrap/src/transforms/RemoveFieldsWithDirective.ts index f3f79f76022..873c30bb087 100644 --- a/packages/wrap/src/transforms/RemoveFieldsWithDirective.ts +++ b/packages/wrap/src/transforms/RemoveFieldsWithDirective.ts @@ -17,8 +17,7 @@ export default class RemoveFieldsWithDirective implements Transform { const valueMap = getDirectives(originalSchema, fieldConfig); return !Object.keys(valueMap).some( directiveName => - ((this.directiveName instanceof RegExp && this.directiveName.test(directiveName)) || - this.directiveName === directiveName) && + valueMatchesCriteria(directiveName, this.directiveName) && ((Array.isArray(valueMap[directiveName]) && valueMap[directiveName].some((value: any) => valueMatchesCriteria(value, this.args))) || valueMatchesCriteria(valueMap[directiveName], this.args)) From eeafdad0ba5cfb974a9c4bbdd3a827ee53e4a42e Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Sun, 23 Aug 2020 22:03:47 -0400 Subject: [PATCH 28/28] renaming. --- packages/stitch/src/.DS_Store | Bin 6148 -> 0 bytes ...ectives.ts => FilterObjectFieldDirectives.ts} | 2 +- ...tions.ts => RemoveObjectFieldDeprecations.ts} | 8 ++++---- ...ectives.ts => RemoveObjectFieldDirectives.ts} | 8 ++++---- ...n.ts => RemoveObjectFieldsWithDeprecation.ts} | 2 +- ...ive.ts => RemoveObjectFieldsWithDirective.ts} | 2 +- packages/wrap/src/transforms/index.ts | 10 +++++----- ...transformFilterObjectFieldDirectives.test.ts} | 6 +++--- ...ansformRemoveObjectFieldDeprecations.test.ts} | 8 ++++---- ...transformRemoveObjectFieldDirectives.test.ts} | 12 ++++++------ ...ormRemoveObjectFieldsWithDeprecation.test.ts} | 8 ++++---- ...sformRemoveObjectFieldsWithDirective.test.ts} | 12 ++++++------ website/docs/stitch-type-merging.md | 8 ++++---- 13 files changed, 43 insertions(+), 43 deletions(-) delete mode 100644 packages/stitch/src/.DS_Store rename packages/wrap/src/transforms/{FilterFieldDirectives.ts => FilterObjectFieldDirectives.ts} (94%) rename packages/wrap/src/transforms/{RemoveFieldDeprecations.ts => RemoveObjectFieldDeprecations.ts} (74%) rename packages/wrap/src/transforms/{RemoveFieldDirectives.ts => RemoveObjectFieldDirectives.ts} (60%) rename packages/wrap/src/transforms/{RemoveFieldsWithDeprecation.ts => RemoveObjectFieldsWithDeprecation.ts} (90%) rename packages/wrap/src/transforms/{RemoveFieldsWithDirective.ts => RemoveObjectFieldsWithDirective.ts} (94%) rename packages/wrap/tests/{transformFilterFieldDirectives.test.ts => transformFilterObjectFieldDirectives.test.ts} (77%) rename packages/wrap/tests/{transformRemoveFieldDeprecations.test.ts => transformRemoveObjectFieldDeprecations.test.ts} (83%) rename packages/wrap/tests/{transformRemoveFieldDirectives.test.ts => transformRemoveObjectFieldDirectives.test.ts} (86%) rename packages/wrap/tests/{transformRemoveDeprecatedFields.test.ts => transformRemoveObjectFieldsWithDeprecation.test.ts} (77%) rename packages/wrap/tests/{transformRemoveFieldsWithDirective.test.ts => transformRemoveObjectFieldsWithDirective.test.ts} (84%) diff --git a/packages/stitch/src/.DS_Store b/packages/stitch/src/.DS_Store deleted file mode 100644 index 55fd88998090877c9753006396b116046ba6b288..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyG{c^3>-s>2%3~B_ZRqsRg`=|egGsC8n{T|QD2qs%BL}Y2oaqSX=u<`vS-)p z+0#vNJ_E4T`{Nz305GRJ;^<*&e(pZ8tI8OW&O2VQ!yX5`aCMnE_Xcm|JYe|F|JWXe z=l$@McVBjys-%DvkOERb3P^!p74Y6mo8KlXN&zV#1-=yU??a boolean; constructor(filter: (dirName: string, dirValue: any) => boolean) { diff --git a/packages/wrap/src/transforms/RemoveFieldDeprecations.ts b/packages/wrap/src/transforms/RemoveObjectFieldDeprecations.ts similarity index 74% rename from packages/wrap/src/transforms/RemoveFieldDeprecations.ts rename to packages/wrap/src/transforms/RemoveObjectFieldDeprecations.ts index 7f5bb01b2ce..164d2c31704 100644 --- a/packages/wrap/src/transforms/RemoveFieldDeprecations.ts +++ b/packages/wrap/src/transforms/RemoveObjectFieldDeprecations.ts @@ -1,14 +1,14 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; -import { FilterFieldDirectives, TransformObjectFields } from '@graphql-tools/wrap'; +import { FilterObjectFieldDirectives, TransformObjectFields } from '@graphql-tools/wrap'; -export default class RemoveFieldDeprecations implements Transform { - private readonly removeDirectives: FilterFieldDirectives; +export default class RemoveObjectFieldDeprecations implements Transform { + private readonly removeDirectives: FilterObjectFieldDirectives; private readonly removeDeprecations: TransformObjectFields; constructor(reason: string | RegExp) { const args = { reason }; - this.removeDirectives = new FilterFieldDirectives((dirName: string, dirValue: any) => { + this.removeDirectives = new FilterObjectFieldDirectives((dirName: string, dirValue: any) => { return !(dirName === 'deprecated' && valueMatchesCriteria(dirValue, args)); }); this.removeDeprecations = new TransformObjectFields( diff --git a/packages/wrap/src/transforms/RemoveFieldDirectives.ts b/packages/wrap/src/transforms/RemoveObjectFieldDirectives.ts similarity index 60% rename from packages/wrap/src/transforms/RemoveFieldDirectives.ts rename to packages/wrap/src/transforms/RemoveObjectFieldDirectives.ts index 9db89290a86..280bd6066e2 100644 --- a/packages/wrap/src/transforms/RemoveFieldDirectives.ts +++ b/packages/wrap/src/transforms/RemoveObjectFieldDirectives.ts @@ -1,12 +1,12 @@ import { GraphQLSchema } from 'graphql'; import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; -import { FilterFieldDirectives } from '@graphql-tools/wrap'; +import { FilterObjectFieldDirectives } from '@graphql-tools/wrap'; -export default class RemoveFieldDirectives implements Transform { - private readonly transformer: FilterFieldDirectives; +export default class RemoveObjectFieldDirectives implements Transform { + private readonly transformer: FilterObjectFieldDirectives; constructor(directiveName: string | RegExp, args: Record = {}) { - this.transformer = new FilterFieldDirectives((dirName: string, dirValue: any) => { + this.transformer = new FilterObjectFieldDirectives((dirName: string, dirValue: any) => { return !(valueMatchesCriteria(dirName, directiveName) && valueMatchesCriteria(dirValue, args)); }); } diff --git a/packages/wrap/src/transforms/RemoveFieldsWithDeprecation.ts b/packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts similarity index 90% rename from packages/wrap/src/transforms/RemoveFieldsWithDeprecation.ts rename to packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts index 34cce8c442b..5ad4d922d49 100644 --- a/packages/wrap/src/transforms/RemoveFieldsWithDeprecation.ts +++ b/packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts @@ -2,7 +2,7 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; import { FilterObjectFields } from '@graphql-tools/wrap'; -export default class RemoveFieldsWithDeprecation implements Transform { +export default class RemoveObjectFieldsWithDeprecation implements Transform { private readonly transformer: FilterObjectFields; constructor(reason: string | RegExp) { diff --git a/packages/wrap/src/transforms/RemoveFieldsWithDirective.ts b/packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts similarity index 94% rename from packages/wrap/src/transforms/RemoveFieldsWithDirective.ts rename to packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts index 873c30bb087..04624d768cc 100644 --- a/packages/wrap/src/transforms/RemoveFieldsWithDirective.ts +++ b/packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts @@ -2,7 +2,7 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; import { Transform, getDirectives, valueMatchesCriteria } from '@graphql-tools/utils'; import { FilterObjectFields } from '@graphql-tools/wrap'; -export default class RemoveFieldsWithDirective implements Transform { +export default class RemoveObjectFieldsWithDirective implements Transform { private readonly directiveName: string | RegExp; private readonly args: Record; diff --git a/packages/wrap/src/transforms/index.ts b/packages/wrap/src/transforms/index.ts index 70ca2126c48..e8a32dfb6b4 100644 --- a/packages/wrap/src/transforms/index.ts +++ b/packages/wrap/src/transforms/index.ts @@ -17,11 +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 FilterFieldDirectives } from './FilterFieldDirectives'; -export { default as RemoveFieldDirectives } from './RemoveFieldDirectives'; -export { default as RemoveFieldsWithDirective } from './RemoveFieldsWithDirective'; -export { default as RemoveFieldDeprecations } from './RemoveFieldDeprecations'; -export { default as RemoveFieldsWithDeprecation } from './RemoveFieldsWithDeprecation'; +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'; diff --git a/packages/wrap/tests/transformFilterFieldDirectives.test.ts b/packages/wrap/tests/transformFilterObjectFieldDirectives.test.ts similarity index 77% rename from packages/wrap/tests/transformFilterFieldDirectives.test.ts rename to packages/wrap/tests/transformFilterObjectFieldDirectives.test.ts index 3736146c297..d1d9fd7fbc2 100644 --- a/packages/wrap/tests/transformFilterFieldDirectives.test.ts +++ b/packages/wrap/tests/transformFilterObjectFieldDirectives.test.ts @@ -1,7 +1,7 @@ -import { wrapSchema, FilterFieldDirectives } from '@graphql-tools/wrap'; +import { wrapSchema, FilterObjectFieldDirectives } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; -describe('FilterFieldDirectives', () => { +describe('FilterObjectFieldDirectives', () => { test('removes unmatched field directives', async () => { const schema = makeExecutableSchema({ typeDefs: ` @@ -17,7 +17,7 @@ describe('FilterFieldDirectives', () => { }); const transformedSchema = wrapSchema(schema, [ - new FilterFieldDirectives((dirName: string, dirValue: any) => dirName === 'keep' && dirValue.arg !== 1) + new FilterObjectFieldDirectives((dirName: string, dirValue: any) => dirName === 'keep' && dirValue.arg !== 1) ]); const fields = transformedSchema.getType('Query').getFields(); diff --git a/packages/wrap/tests/transformRemoveFieldDeprecations.test.ts b/packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts similarity index 83% rename from packages/wrap/tests/transformRemoveFieldDeprecations.test.ts rename to packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts index 351536b85c2..40e02f114c4 100644 --- a/packages/wrap/tests/transformRemoveFieldDeprecations.test.ts +++ b/packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts @@ -1,7 +1,7 @@ -import { wrapSchema, RemoveFieldDeprecations } from '@graphql-tools/wrap'; +import { wrapSchema, RemoveObjectFieldDeprecations } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; -describe('RemoveFieldDeprecations', () => { +describe('RemoveObjectFieldDeprecations', () => { const originalSchema = makeExecutableSchema({ typeDefs: ` type Test { @@ -14,7 +14,7 @@ describe('RemoveFieldDeprecations', () => { test('removes deprecations by reason', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldDeprecations('remove this') + new RemoveObjectFieldDeprecations('remove this') ]); const fields = transformedSchema.getType('Test').getFields(); @@ -26,7 +26,7 @@ describe('RemoveFieldDeprecations', () => { test('removes deprecations by reason regex', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldDeprecations(/remove/) + new RemoveObjectFieldDeprecations(/remove/) ]); const fields = transformedSchema.getType('Test').getFields(); diff --git a/packages/wrap/tests/transformRemoveFieldDirectives.test.ts b/packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts similarity index 86% rename from packages/wrap/tests/transformRemoveFieldDirectives.test.ts rename to packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts index 1c2edbe9ea2..74c49bfe84d 100644 --- a/packages/wrap/tests/transformRemoveFieldDirectives.test.ts +++ b/packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts @@ -1,7 +1,7 @@ -import { wrapSchema, RemoveFieldDirectives } from '@graphql-tools/wrap'; +import { wrapSchema, RemoveObjectFieldDirectives } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; -describe('RemoveFieldDirectives', () => { +describe('RemoveObjectFieldDirectives', () => { const originalSchema = makeExecutableSchema({ typeDefs: ` directive @alpha(arg: String) on FIELD_DEFINITION @@ -18,7 +18,7 @@ describe('RemoveFieldDirectives', () => { test('removes directives by name', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldDirectives('alpha') + new RemoveObjectFieldDirectives('alpha') ]); const fields = transformedSchema.getType('Test').getFields(); @@ -30,7 +30,7 @@ describe('RemoveFieldDirectives', () => { test('removes directives by name regex', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldDirectives(/^alp/) + new RemoveObjectFieldDirectives(/^alp/) ]); const fields = transformedSchema.getType('Test').getFields(); @@ -42,7 +42,7 @@ describe('RemoveFieldDirectives', () => { test('removes directives by argument', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldDirectives(/.+/, { arg: 'remove this' }) + new RemoveObjectFieldDirectives(/.+/, { arg: 'remove this' }) ]); const fields = transformedSchema.getType('Test').getFields(); @@ -54,7 +54,7 @@ describe('RemoveFieldDirectives', () => { test('removes directives by argument regex', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldDirectives(/.+/, { arg: /remove/ }) + new RemoveObjectFieldDirectives(/.+/, { arg: /remove/ }) ]); const fields = transformedSchema.getType('Test').getFields(); diff --git a/packages/wrap/tests/transformRemoveDeprecatedFields.test.ts b/packages/wrap/tests/transformRemoveObjectFieldsWithDeprecation.test.ts similarity index 77% rename from packages/wrap/tests/transformRemoveDeprecatedFields.test.ts rename to packages/wrap/tests/transformRemoveObjectFieldsWithDeprecation.test.ts index 1294623b4a2..37713497386 100644 --- a/packages/wrap/tests/transformRemoveDeprecatedFields.test.ts +++ b/packages/wrap/tests/transformRemoveObjectFieldsWithDeprecation.test.ts @@ -1,7 +1,7 @@ -import { wrapSchema, RemoveFieldsWithDeprecation } from '@graphql-tools/wrap'; +import { wrapSchema, RemoveObjectFieldsWithDeprecation } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; -describe('RemoveFieldsWithDeprecation', () => { +describe('RemoveObjectFieldsWithDeprecation', () => { const originalSchema = makeExecutableSchema({ typeDefs: ` type Test { @@ -14,7 +14,7 @@ describe('RemoveFieldsWithDeprecation', () => { test('removes deprecated fields by reason', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldsWithDeprecation('remove this') + new RemoveObjectFieldsWithDeprecation('remove this') ]); const fields = transformedSchema.getType('Test').getFields(); @@ -24,7 +24,7 @@ describe('RemoveFieldsWithDeprecation', () => { test('removes deprecated fields by reason regex', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldsWithDeprecation(/remove/) + new RemoveObjectFieldsWithDeprecation(/remove/) ]); const fields = transformedSchema.getType('Test').getFields(); diff --git a/packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts b/packages/wrap/tests/transformRemoveObjectFieldsWithDirective.test.ts similarity index 84% rename from packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts rename to packages/wrap/tests/transformRemoveObjectFieldsWithDirective.test.ts index bfaebb6df23..6da71fc6e8c 100644 --- a/packages/wrap/tests/transformRemoveFieldsWithDirective.test.ts +++ b/packages/wrap/tests/transformRemoveObjectFieldsWithDirective.test.ts @@ -1,7 +1,7 @@ -import { wrapSchema, RemoveFieldsWithDirective } from '@graphql-tools/wrap'; +import { wrapSchema, RemoveObjectFieldsWithDirective } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; -describe('RemoveFieldsWithDirective', () => { +describe('RemoveObjectFieldsWithDirective', () => { const originalSchema = makeExecutableSchema({ typeDefs: ` directive @alpha(arg: String) on FIELD_DEFINITION @@ -19,7 +19,7 @@ describe('RemoveFieldsWithDirective', () => { test('removes directive fields by name', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldsWithDirective('alpha') + new RemoveObjectFieldsWithDirective('alpha') ]); const fields = transformedSchema.getType('Test').getFields(); @@ -31,7 +31,7 @@ describe('RemoveFieldsWithDirective', () => { test('removes directive fields by name regex', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldsWithDirective(/^alp/) + new RemoveObjectFieldsWithDirective(/^alp/) ]); const fields = transformedSchema.getType('Test').getFields(); @@ -43,7 +43,7 @@ describe('RemoveFieldsWithDirective', () => { test('removes directive fields by argument', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldsWithDirective(/.+/, { arg: 'remove this' }) + new RemoveObjectFieldsWithDirective(/.+/, { arg: 'remove this' }) ]); const fields = transformedSchema.getType('Test').getFields(); @@ -55,7 +55,7 @@ describe('RemoveFieldsWithDirective', () => { test('removes directive fields by argument regex', async () => { const transformedSchema = wrapSchema(originalSchema, [ - new RemoveFieldsWithDirective(/.+/, { arg: /remove/ }) + new RemoveObjectFieldsWithDirective(/.+/, { arg: /remove/ }) ]); const fields = transformedSchema.getType('Test').getFields(); diff --git a/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index 06574f8c9ac..1d4f83b350c 100644 --- a/website/docs/stitch-type-merging.md +++ b/website/docs/stitch-type-merging.md @@ -397,7 +397,7 @@ However, you may notice that both `buyerId` and `sellerId` keys are _always_ req One disadvantage of this pattern is that we end up with clutter—`buyerId`/`sellerId` are extra fields, and `buyer`/`seller` fields have gateway dependencies. To tidy things up, we can aggressively deprecate these fields in subschemas and then remove/normalize their behavior in the gateway using available transforms: ```js -import { RemoveFieldsWithDeprecation, RemoveFieldDeprecations } from '@graphql-tools/wrap'; +import { RemoveObjectFieldsWithDeprecation, RemoveObjectFieldDeprecations } from '@graphql-tools/wrap'; const listingsSchema = makeExecutableSchema({ typeDefs: ` @@ -429,19 +429,19 @@ const gatewaySchema = stitchSchemas({ subschemas: [ { schema: listingsSchema, - transforms: [new RemoveFieldsWithDeprecation('stitching use only')], + transforms: [new RemoveObjectFieldsWithDeprecation('stitching use only')], merge: { ... } }, { schema: usersSchema, - transforms: [new RemoveFieldDeprecations('gateway access only')], + transforms: [new RemoveObjectFieldDeprecations('gateway access only')], merge: { ... } }, ], }); ``` -Cleanup can also be performed on custom directives using `RemoveFieldsWithDirective` and/or `RemoveFieldDirectives` transforms. +Cleanup of custom directives may also be performed with `RemoveObjectFieldsWithDirective` and `RemoveObjectFieldDirectives`. ### Federation services