diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 734687b9297..1fda6639b9d 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -48,3 +48,5 @@ export * from './errors'; export * from './toConfig'; export * from './observableToAsyncIterable'; export * from './visitResult'; +export * from './getArgumentValues'; +export * from './valueMatchesCriteria'; diff --git a/packages/utils/src/valueMatchesCriteria.ts b/packages/utils/src/valueMatchesCriteria.ts new file mode 100644 index 00000000000..1da37d7760d --- /dev/null +++ b/packages/utils/src/valueMatchesCriteria.ts @@ -0,0 +1,17 @@ +export function valueMatchesCriteria(value: any, criteria: any): boolean { + if (value == null) { + return value === criteria; + } else if (Array.isArray(value)) { + return Array.isArray(criteria) && value.every((val, index) => valueMatchesCriteria(val, criteria[index])); + } else if (typeof value === 'object') { + return ( + typeof criteria === 'object' && + criteria && + Object.keys(criteria).every(propertyName => valueMatchesCriteria(value[propertyName], criteria[propertyName])) + ); + } else if (criteria instanceof RegExp) { + return criteria.test(value); + } + + return value === criteria; +} diff --git a/packages/utils/tests/valueMatchesCriteria.test.ts b/packages/utils/tests/valueMatchesCriteria.test.ts new file mode 100644 index 00000000000..cd6c69cb8e5 --- /dev/null +++ b/packages/utils/tests/valueMatchesCriteria.test.ts @@ -0,0 +1,59 @@ +import { valueMatchesCriteria } from '../src/index'; + +describe('valueMatchesCriteria', () => { + test('matches empty values', () => { + expect(valueMatchesCriteria(undefined, undefined)).toBe(true); + expect(valueMatchesCriteria(undefined, null)).toBe(false); + expect(valueMatchesCriteria(null, null)).toBe(true); + }); + + test('matches primitives', () => { + expect(valueMatchesCriteria(1, 1)).toBe(true); + expect(valueMatchesCriteria(1, 2)).toBe(false); + expect(valueMatchesCriteria('a', 'a')).toBe(true); + expect(valueMatchesCriteria('a', 'b')).toBe(false); + expect(valueMatchesCriteria(false, false)).toBe(true); + expect(valueMatchesCriteria(false, true)).toBe(false); + }); + + test('matches empty object values', () => { + expect(valueMatchesCriteria({}, {})).toBe(true); + }); + + test('matches value object with varying specificity', () => { + const dirValue = { reason: 'reason', also: 'also' }; + + expect(valueMatchesCriteria(dirValue, {})).toBe(true); + expect(valueMatchesCriteria(dirValue, { reason: 'reason' })).toBe(true); + expect(valueMatchesCriteria(dirValue, { reason: 'reason', also: 'also' })).toBe(true); + expect(valueMatchesCriteria(dirValue, { reason: 'reason', and: 'and' })).toBe(false); + expect(valueMatchesCriteria(dirValue, { this: 'this' })).toBe(false); + expect(valueMatchesCriteria(dirValue, { reason: 'this' })).toBe(false); + }); + + test('matches value objects recursively', () => { + const dirValue = { reason: 'reason', also: { a: 1, b: 2 } }; + + expect(valueMatchesCriteria(dirValue, { reason: 'reason' })).toBe(true); + expect(valueMatchesCriteria(dirValue, { also: {} })).toBe(true); + expect(valueMatchesCriteria(dirValue, { also: { a: 1 } })).toBe(true); + expect(valueMatchesCriteria(dirValue, { also: { a: 1, b: 2 } })).toBe(true); + expect(valueMatchesCriteria(dirValue, { also: { a: 1, b: 0 } })).toBe(false); + expect(valueMatchesCriteria(dirValue, { also: { c: 1 } })).toBe(false); + }); + + test('matches value arrays', () => { + const dirValue = [23, { hello: true, world: false }]; + + expect(valueMatchesCriteria(dirValue, [23, { hello: true }])).toBe(true); + expect(valueMatchesCriteria(dirValue, [23, { world: false }])).toBe(true); + expect(valueMatchesCriteria(dirValue, [{ hello: true }, 23])).toBe(false); + }); + + test('matches value with regex', () => { + const dirValue = { reason: 'requires: id' }; + + expect(valueMatchesCriteria(dirValue, { reason: /^requires:/ })).toBe(true); + expect(valueMatchesCriteria(dirValue, { reason: /^required:/ })).toBe(false); + }); +}); diff --git a/packages/wrap/src/transforms/FilterObjectFieldDirectives.ts b/packages/wrap/src/transforms/FilterObjectFieldDirectives.ts new file mode 100644 index 00000000000..cc7fc0f6d7c --- /dev/null +++ b/packages/wrap/src/transforms/FilterObjectFieldDirectives.ts @@ -0,0 +1,36 @@ +import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; +import { Transform, getArgumentValues } from '@graphql-tools/utils'; +import TransformObjectFields from './TransformObjectFields'; + +export default class FilterObjectFieldDirectives implements Transform { + private readonly filter: (dirName: string, dirValue: any) => boolean; + + constructor(filter: (dirName: string, dirValue: any) => boolean) { + this.filter = filter; + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + const transformer = new TransformObjectFields( + (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { + const keepDirectives = fieldConfig.astNode.directives.filter(dir => { + const directiveDef = originalSchema.getDirective(dir.name.value); + const directiveValue = directiveDef ? getArgumentValues(directiveDef, dir) : undefined; + return this.filter(dir.name.value, directiveValue); + }); + + if (keepDirectives.length !== fieldConfig.astNode.directives.length) { + fieldConfig = { + ...fieldConfig, + astNode: { + ...fieldConfig.astNode, + directives: keepDirectives, + }, + }; + return fieldConfig; + } + } + ); + + return transformer.transformSchema(originalSchema); + } +} diff --git a/packages/wrap/src/transforms/RemoveObjectFieldDeprecations.ts b/packages/wrap/src/transforms/RemoveObjectFieldDeprecations.ts new file mode 100644 index 00000000000..164d2c31704 --- /dev/null +++ b/packages/wrap/src/transforms/RemoveObjectFieldDeprecations.ts @@ -0,0 +1,28 @@ +import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; +import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; +import { FilterObjectFieldDirectives, TransformObjectFields } from '@graphql-tools/wrap'; + +export default class RemoveObjectFieldDeprecations implements Transform { + private readonly removeDirectives: FilterObjectFieldDirectives; + private readonly removeDeprecations: TransformObjectFields; + + constructor(reason: string | RegExp) { + const args = { reason }; + this.removeDirectives = new FilterObjectFieldDirectives((dirName: string, dirValue: any) => { + return !(dirName === 'deprecated' && valueMatchesCriteria(dirValue, args)); + }); + this.removeDeprecations = new TransformObjectFields( + (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { + if (fieldConfig.deprecationReason && valueMatchesCriteria(fieldConfig.deprecationReason, reason)) { + fieldConfig = { ...fieldConfig }; + delete fieldConfig.deprecationReason; + } + return fieldConfig; + } + ); + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return this.removeDeprecations.transformSchema(this.removeDirectives.transformSchema(originalSchema)); + } +} diff --git a/packages/wrap/src/transforms/RemoveObjectFieldDirectives.ts b/packages/wrap/src/transforms/RemoveObjectFieldDirectives.ts new file mode 100644 index 00000000000..280bd6066e2 --- /dev/null +++ b/packages/wrap/src/transforms/RemoveObjectFieldDirectives.ts @@ -0,0 +1,17 @@ +import { GraphQLSchema } from 'graphql'; +import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; +import { FilterObjectFieldDirectives } from '@graphql-tools/wrap'; + +export default class RemoveObjectFieldDirectives implements Transform { + private readonly transformer: FilterObjectFieldDirectives; + + constructor(directiveName: string | RegExp, args: Record = {}) { + this.transformer = new FilterObjectFieldDirectives((dirName: string, dirValue: any) => { + return !(valueMatchesCriteria(dirName, directiveName) && valueMatchesCriteria(dirValue, args)); + }); + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return this.transformer.transformSchema(originalSchema); + } +} diff --git a/packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts b/packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts new file mode 100644 index 00000000000..5ad4d922d49 --- /dev/null +++ b/packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts @@ -0,0 +1,22 @@ +import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; +import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; +import { FilterObjectFields } from '@graphql-tools/wrap'; + +export default class RemoveObjectFieldsWithDeprecation implements Transform { + private readonly transformer: FilterObjectFields; + + constructor(reason: string | RegExp) { + this.transformer = new FilterObjectFields( + (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { + if (fieldConfig.deprecationReason) { + return !valueMatchesCriteria(fieldConfig.deprecationReason, reason); + } + return true; + } + ); + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + return this.transformer.transformSchema(originalSchema); + } +} diff --git a/packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts b/packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts new file mode 100644 index 00000000000..04624d768cc --- /dev/null +++ b/packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts @@ -0,0 +1,30 @@ +import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; +import { Transform, getDirectives, valueMatchesCriteria } from '@graphql-tools/utils'; +import { FilterObjectFields } from '@graphql-tools/wrap'; + +export default class RemoveObjectFieldsWithDirective implements Transform { + private readonly directiveName: string | RegExp; + private readonly args: Record; + + constructor(directiveName: string | RegExp, args: Record = {}) { + this.directiveName = directiveName; + this.args = args; + } + + public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { + const transformer = new FilterObjectFields( + (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { + const valueMap = getDirectives(originalSchema, fieldConfig); + return !Object.keys(valueMap).some( + directiveName => + valueMatchesCriteria(directiveName, this.directiveName) && + ((Array.isArray(valueMap[directiveName]) && + valueMap[directiveName].some((value: any) => valueMatchesCriteria(value, this.args))) || + valueMatchesCriteria(valueMap[directiveName], this.args)) + ); + } + ); + + return transformer.transformSchema(originalSchema); + } +} diff --git a/packages/wrap/src/transforms/index.ts b/packages/wrap/src/transforms/index.ts index 1bae4f02ae7..e8a32dfb6b4 100644 --- a/packages/wrap/src/transforms/index.ts +++ b/packages/wrap/src/transforms/index.ts @@ -17,6 +17,11 @@ export { default as FilterInputObjectFields } from './FilterInputObjectFields'; export { default as MapLeafValues } from './MapLeafValues'; export { default as TransformEnumValues } from './TransformEnumValues'; export { default as TransformQuery } from './TransformQuery'; +export { default as FilterObjectFieldDirectives } from './FilterObjectFieldDirectives'; +export { default as RemoveObjectFieldDirectives } from './RemoveObjectFieldDirectives'; +export { default as RemoveObjectFieldsWithDirective } from './RemoveObjectFieldsWithDirective'; +export { default as RemoveObjectFieldDeprecations } from './RemoveObjectFieldDeprecations'; +export { default as RemoveObjectFieldsWithDeprecation } from './RemoveObjectFieldsWithDeprecation'; export { default as ExtendSchema } from './ExtendSchema'; export { default as PruneSchema } from './PruneSchema'; diff --git a/packages/wrap/tests/transformFilterObjectFieldDirectives.test.ts b/packages/wrap/tests/transformFilterObjectFieldDirectives.test.ts new file mode 100644 index 00000000000..d1d9fd7fbc2 --- /dev/null +++ b/packages/wrap/tests/transformFilterObjectFieldDirectives.test.ts @@ -0,0 +1,29 @@ +import { wrapSchema, FilterObjectFieldDirectives } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('FilterObjectFieldDirectives', () => { + test('removes unmatched field directives', async () => { + const schema = makeExecutableSchema({ + typeDefs: ` + directive @remove on FIELD_DEFINITION + directive @keep(arg: Int) on FIELD_DEFINITION + type Query { + alpha:String @remove + bravo:String @keep + charlie:String @keep(arg:1) + delta:String @keep(arg:2) + } + ` + }); + + const transformedSchema = wrapSchema(schema, [ + new FilterObjectFieldDirectives((dirName: string, dirValue: any) => dirName === 'keep' && dirValue.arg !== 1) + ]); + + const fields = transformedSchema.getType('Query').getFields(); + expect(fields.alpha.astNode.directives.length).toEqual(0); + expect(fields.bravo.astNode.directives.length).toEqual(1); + expect(fields.charlie.astNode.directives.length).toEqual(0); + expect(fields.delta.astNode.directives.length).toEqual(1); + }); +}); diff --git a/packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts b/packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts new file mode 100644 index 00000000000..40e02f114c4 --- /dev/null +++ b/packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts @@ -0,0 +1,38 @@ +import { wrapSchema, RemoveObjectFieldDeprecations } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('RemoveObjectFieldDeprecations', () => { + const originalSchema = makeExecutableSchema({ + typeDefs: ` + type Test { + id: ID! + first: String! @deprecated(reason: "do not remove") + second: String! @deprecated(reason: "remove this") + } + ` + }); + + test('removes deprecations by reason', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldDeprecations('remove this') + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.first.deprecationReason).toEqual('do not remove'); + expect(fields.second.deprecationReason).toBeUndefined(); + expect(fields.first.astNode.directives.length).toEqual(1); + expect(fields.second.astNode.directives.length).toEqual(0); + }); + + test('removes deprecations by reason regex', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldDeprecations(/remove/) + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.first.deprecationReason).toBeUndefined(); + expect(fields.second.deprecationReason).toBeUndefined(); + expect(fields.first.astNode.directives.length).toEqual(0); + expect(fields.second.astNode.directives.length).toEqual(0); + }); +}); diff --git a/packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts b/packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts new file mode 100644 index 00000000000..74c49bfe84d --- /dev/null +++ b/packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts @@ -0,0 +1,66 @@ +import { wrapSchema, RemoveObjectFieldDirectives } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('RemoveObjectFieldDirectives', () => { + const originalSchema = makeExecutableSchema({ + typeDefs: ` + directive @alpha(arg: String) on FIELD_DEFINITION + directive @bravo(arg: String) on FIELD_DEFINITION + + type Test { + id: ID! @bravo(arg: "remove this") + first: String! @alpha(arg: "do not remove") + second: String! @alpha(arg: "remove this") + third: String @alpha(arg: "remove this") + } + ` + }); + + test('removes directives by name', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldDirectives('alpha') + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.id.astNode.directives.length).toEqual(1); + expect(fields.first.astNode.directives.length).toEqual(0); + expect(fields.second.astNode.directives.length).toEqual(0); + expect(fields.third.astNode.directives.length).toEqual(0); + }); + + test('removes directives by name regex', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldDirectives(/^alp/) + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.id.astNode.directives.length).toEqual(1); + expect(fields.first.astNode.directives.length).toEqual(0); + expect(fields.second.astNode.directives.length).toEqual(0); + expect(fields.third.astNode.directives.length).toEqual(0); + }); + + test('removes directives by argument', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldDirectives(/.+/, { arg: 'remove this' }) + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.id.astNode.directives.length).toEqual(0); + expect(fields.first.astNode.directives.length).toEqual(1); + expect(fields.second.astNode.directives.length).toEqual(0); + expect(fields.third.astNode.directives.length).toEqual(0); + }); + + test('removes directives by argument regex', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldDirectives(/.+/, { arg: /remove/ }) + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.id.astNode.directives.length).toEqual(0); + expect(fields.first.astNode.directives.length).toEqual(0); + expect(fields.second.astNode.directives.length).toEqual(0); + expect(fields.third.astNode.directives.length).toEqual(0); + }); +}); diff --git a/packages/wrap/tests/transformRemoveObjectFieldsWithDeprecation.test.ts b/packages/wrap/tests/transformRemoveObjectFieldsWithDeprecation.test.ts new file mode 100644 index 00000000000..37713497386 --- /dev/null +++ b/packages/wrap/tests/transformRemoveObjectFieldsWithDeprecation.test.ts @@ -0,0 +1,34 @@ +import { wrapSchema, RemoveObjectFieldsWithDeprecation } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('RemoveObjectFieldsWithDeprecation', () => { + const originalSchema = makeExecutableSchema({ + typeDefs: ` + type Test { + id: ID! + first: String! @deprecated(reason: "do not remove") + second: String! @deprecated(reason: "remove this") + } + ` + }); + + test('removes deprecated fields by reason', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldsWithDeprecation('remove this') + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.first).toBeDefined(); + expect(fields.second).toBeUndefined(); + }); + + test('removes deprecated fields by reason regex', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldsWithDeprecation(/remove/) + ]); + + const fields = transformedSchema.getType('Test').getFields(); + expect(fields.first).toBeUndefined(); + expect(fields.second).toBeUndefined(); + }); +}); diff --git a/packages/wrap/tests/transformRemoveObjectFieldsWithDirective.test.ts b/packages/wrap/tests/transformRemoveObjectFieldsWithDirective.test.ts new file mode 100644 index 00000000000..6da71fc6e8c --- /dev/null +++ b/packages/wrap/tests/transformRemoveObjectFieldsWithDirective.test.ts @@ -0,0 +1,67 @@ +import { wrapSchema, RemoveObjectFieldsWithDirective } from '@graphql-tools/wrap'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('RemoveObjectFieldsWithDirective', () => { + 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 directive fields by name', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldsWithDirective('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 directive fields by name regex', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldsWithDirective(/^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 directive fields by argument', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldsWithDirective(/.+/, { 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 directive fields by argument regex', async () => { + const transformedSchema = wrapSchema(originalSchema, [ + new RemoveObjectFieldsWithDirective(/.+/, { 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/website/docs/stitch-type-merging.md b/website/docs/stitch-type-merging.md index 3edac4a796f..1d4f83b350c 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,54 @@ 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 { RemoveObjectFieldsWithDeprecation, RemoveObjectFieldDeprecations } from '@graphql-tools/wrap'; + +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 RemoveObjectFieldsWithDeprecation('stitching use only')], + merge: { ... } + }, + { + schema: usersSchema, + transforms: [new RemoveObjectFieldDeprecations('gateway access only')], + merge: { ... } + }, + ], +}); +``` + +Cleanup of custom directives may also be performed with `RemoveObjectFieldsWithDirective` and `RemoveObjectFieldDirectives`. ### Federation services