-
-
Notifications
You must be signed in to change notification settings - Fork 796
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[deploy_website] feat(transforms): normalizing stitched subschemas (#…
…1925) Adds package support and a documented approach for handling Federation-style stitching patterns that result in field oddities outside of the gateway context.
- Loading branch information
Showing
15 changed files
with
501 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
export function valueMatchesCriteria(value: any, criteria: any): boolean { | ||
if (value == null) { | ||
return value === criteria; | ||
} else if (Array.isArray(value)) { | ||
return Array.isArray(criteria) && value.every((val, index) => valueMatchesCriteria(val, criteria[index])); | ||
} else if (typeof value === 'object') { | ||
return ( | ||
typeof criteria === 'object' && | ||
criteria && | ||
Object.keys(criteria).every(propertyName => valueMatchesCriteria(value[propertyName], criteria[propertyName])) | ||
); | ||
} else if (criteria instanceof RegExp) { | ||
return criteria.test(value); | ||
} | ||
|
||
return value === criteria; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { valueMatchesCriteria } from '../src/index'; | ||
|
||
describe('valueMatchesCriteria', () => { | ||
test('matches empty values', () => { | ||
expect(valueMatchesCriteria(undefined, undefined)).toBe(true); | ||
expect(valueMatchesCriteria(undefined, null)).toBe(false); | ||
expect(valueMatchesCriteria(null, null)).toBe(true); | ||
}); | ||
|
||
test('matches primitives', () => { | ||
expect(valueMatchesCriteria(1, 1)).toBe(true); | ||
expect(valueMatchesCriteria(1, 2)).toBe(false); | ||
expect(valueMatchesCriteria('a', 'a')).toBe(true); | ||
expect(valueMatchesCriteria('a', 'b')).toBe(false); | ||
expect(valueMatchesCriteria(false, false)).toBe(true); | ||
expect(valueMatchesCriteria(false, true)).toBe(false); | ||
}); | ||
|
||
test('matches empty object values', () => { | ||
expect(valueMatchesCriteria({}, {})).toBe(true); | ||
}); | ||
|
||
test('matches value object with varying specificity', () => { | ||
const dirValue = { reason: 'reason', also: 'also' }; | ||
|
||
expect(valueMatchesCriteria(dirValue, {})).toBe(true); | ||
expect(valueMatchesCriteria(dirValue, { reason: 'reason' })).toBe(true); | ||
expect(valueMatchesCriteria(dirValue, { reason: 'reason', also: 'also' })).toBe(true); | ||
expect(valueMatchesCriteria(dirValue, { reason: 'reason', and: 'and' })).toBe(false); | ||
expect(valueMatchesCriteria(dirValue, { this: 'this' })).toBe(false); | ||
expect(valueMatchesCriteria(dirValue, { reason: 'this' })).toBe(false); | ||
}); | ||
|
||
test('matches value objects recursively', () => { | ||
const dirValue = { reason: 'reason', also: { a: 1, b: 2 } }; | ||
|
||
expect(valueMatchesCriteria(dirValue, { reason: 'reason' })).toBe(true); | ||
expect(valueMatchesCriteria(dirValue, { also: {} })).toBe(true); | ||
expect(valueMatchesCriteria(dirValue, { also: { a: 1 } })).toBe(true); | ||
expect(valueMatchesCriteria(dirValue, { also: { a: 1, b: 2 } })).toBe(true); | ||
expect(valueMatchesCriteria(dirValue, { also: { a: 1, b: 0 } })).toBe(false); | ||
expect(valueMatchesCriteria(dirValue, { also: { c: 1 } })).toBe(false); | ||
}); | ||
|
||
test('matches value arrays', () => { | ||
const dirValue = [23, { hello: true, world: false }]; | ||
|
||
expect(valueMatchesCriteria(dirValue, [23, { hello: true }])).toBe(true); | ||
expect(valueMatchesCriteria(dirValue, [23, { world: false }])).toBe(true); | ||
expect(valueMatchesCriteria(dirValue, [{ hello: true }, 23])).toBe(false); | ||
}); | ||
|
||
test('matches value with regex', () => { | ||
const dirValue = { reason: 'requires: id' }; | ||
|
||
expect(valueMatchesCriteria(dirValue, { reason: /^requires:/ })).toBe(true); | ||
expect(valueMatchesCriteria(dirValue, { reason: /^required:/ })).toBe(false); | ||
}); | ||
}); |
36 changes: 36 additions & 0 deletions
36
packages/wrap/src/transforms/FilterObjectFieldDirectives.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; | ||
import { Transform, getArgumentValues } from '@graphql-tools/utils'; | ||
import TransformObjectFields from './TransformObjectFields'; | ||
|
||
export default class FilterObjectFieldDirectives implements Transform { | ||
private readonly filter: (dirName: string, dirValue: any) => boolean; | ||
|
||
constructor(filter: (dirName: string, dirValue: any) => boolean) { | ||
this.filter = filter; | ||
} | ||
|
||
public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { | ||
const transformer = new TransformObjectFields( | ||
(_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig<any, any>) => { | ||
const keepDirectives = fieldConfig.astNode.directives.filter(dir => { | ||
const directiveDef = originalSchema.getDirective(dir.name.value); | ||
const directiveValue = directiveDef ? getArgumentValues(directiveDef, dir) : undefined; | ||
return this.filter(dir.name.value, directiveValue); | ||
}); | ||
|
||
if (keepDirectives.length !== fieldConfig.astNode.directives.length) { | ||
fieldConfig = { | ||
...fieldConfig, | ||
astNode: { | ||
...fieldConfig.astNode, | ||
directives: keepDirectives, | ||
}, | ||
}; | ||
return fieldConfig; | ||
} | ||
} | ||
); | ||
|
||
return transformer.transformSchema(originalSchema); | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
packages/wrap/src/transforms/RemoveObjectFieldDeprecations.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; | ||
import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; | ||
import { FilterObjectFieldDirectives, TransformObjectFields } from '@graphql-tools/wrap'; | ||
|
||
export default class RemoveObjectFieldDeprecations implements Transform { | ||
private readonly removeDirectives: FilterObjectFieldDirectives; | ||
private readonly removeDeprecations: TransformObjectFields; | ||
|
||
constructor(reason: string | RegExp) { | ||
const args = { reason }; | ||
this.removeDirectives = new FilterObjectFieldDirectives((dirName: string, dirValue: any) => { | ||
return !(dirName === 'deprecated' && valueMatchesCriteria(dirValue, args)); | ||
}); | ||
this.removeDeprecations = new TransformObjectFields( | ||
(_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig<any, any>) => { | ||
if (fieldConfig.deprecationReason && valueMatchesCriteria(fieldConfig.deprecationReason, reason)) { | ||
fieldConfig = { ...fieldConfig }; | ||
delete fieldConfig.deprecationReason; | ||
} | ||
return fieldConfig; | ||
} | ||
); | ||
} | ||
|
||
public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { | ||
return this.removeDeprecations.transformSchema(this.removeDirectives.transformSchema(originalSchema)); | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
packages/wrap/src/transforms/RemoveObjectFieldDirectives.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { GraphQLSchema } from 'graphql'; | ||
import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; | ||
import { FilterObjectFieldDirectives } from '@graphql-tools/wrap'; | ||
|
||
export default class RemoveObjectFieldDirectives implements Transform { | ||
private readonly transformer: FilterObjectFieldDirectives; | ||
|
||
constructor(directiveName: string | RegExp, args: Record<string, any> = {}) { | ||
this.transformer = new FilterObjectFieldDirectives((dirName: string, dirValue: any) => { | ||
return !(valueMatchesCriteria(dirName, directiveName) && valueMatchesCriteria(dirValue, args)); | ||
}); | ||
} | ||
|
||
public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { | ||
return this.transformer.transformSchema(originalSchema); | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; | ||
import { Transform, valueMatchesCriteria } from '@graphql-tools/utils'; | ||
import { FilterObjectFields } from '@graphql-tools/wrap'; | ||
|
||
export default class RemoveObjectFieldsWithDeprecation implements Transform { | ||
private readonly transformer: FilterObjectFields; | ||
|
||
constructor(reason: string | RegExp) { | ||
this.transformer = new FilterObjectFields( | ||
(_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig<any, any>) => { | ||
if (fieldConfig.deprecationReason) { | ||
return !valueMatchesCriteria(fieldConfig.deprecationReason, reason); | ||
} | ||
return true; | ||
} | ||
); | ||
} | ||
|
||
public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { | ||
return this.transformer.transformSchema(originalSchema); | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; | ||
import { Transform, getDirectives, valueMatchesCriteria } from '@graphql-tools/utils'; | ||
import { FilterObjectFields } from '@graphql-tools/wrap'; | ||
|
||
export default class RemoveObjectFieldsWithDirective implements Transform { | ||
private readonly directiveName: string | RegExp; | ||
private readonly args: Record<string, any>; | ||
|
||
constructor(directiveName: string | RegExp, args: Record<string, any> = {}) { | ||
this.directiveName = directiveName; | ||
this.args = args; | ||
} | ||
|
||
public transformSchema(originalSchema: GraphQLSchema): GraphQLSchema { | ||
const transformer = new FilterObjectFields( | ||
(_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig<any, any>) => { | ||
const valueMap = getDirectives(originalSchema, fieldConfig); | ||
return !Object.keys(valueMap).some( | ||
directiveName => | ||
valueMatchesCriteria(directiveName, this.directiveName) && | ||
((Array.isArray(valueMap[directiveName]) && | ||
valueMap[directiveName].some((value: any) => valueMatchesCriteria(value, this.args))) || | ||
valueMatchesCriteria(valueMap[directiveName], this.args)) | ||
); | ||
} | ||
); | ||
|
||
return transformer.transformSchema(originalSchema); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
packages/wrap/tests/transformFilterObjectFieldDirectives.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { wrapSchema, FilterObjectFieldDirectives } from '@graphql-tools/wrap'; | ||
import { makeExecutableSchema } from '@graphql-tools/schema'; | ||
|
||
describe('FilterObjectFieldDirectives', () => { | ||
test('removes unmatched field directives', async () => { | ||
const schema = makeExecutableSchema({ | ||
typeDefs: ` | ||
directive @remove on FIELD_DEFINITION | ||
directive @keep(arg: Int) on FIELD_DEFINITION | ||
type Query { | ||
alpha:String @remove | ||
bravo:String @keep | ||
charlie:String @keep(arg:1) | ||
delta:String @keep(arg:2) | ||
} | ||
` | ||
}); | ||
|
||
const transformedSchema = wrapSchema(schema, [ | ||
new FilterObjectFieldDirectives((dirName: string, dirValue: any) => dirName === 'keep' && dirValue.arg !== 1) | ||
]); | ||
|
||
const fields = transformedSchema.getType('Query').getFields(); | ||
expect(fields.alpha.astNode.directives.length).toEqual(0); | ||
expect(fields.bravo.astNode.directives.length).toEqual(1); | ||
expect(fields.charlie.astNode.directives.length).toEqual(0); | ||
expect(fields.delta.astNode.directives.length).toEqual(1); | ||
}); | ||
}); |
38 changes: 38 additions & 0 deletions
38
packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { wrapSchema, RemoveObjectFieldDeprecations } from '@graphql-tools/wrap'; | ||
import { makeExecutableSchema } from '@graphql-tools/schema'; | ||
|
||
describe('RemoveObjectFieldDeprecations', () => { | ||
const originalSchema = makeExecutableSchema({ | ||
typeDefs: ` | ||
type Test { | ||
id: ID! | ||
first: String! @deprecated(reason: "do not remove") | ||
second: String! @deprecated(reason: "remove this") | ||
} | ||
` | ||
}); | ||
|
||
test('removes deprecations by reason', async () => { | ||
const transformedSchema = wrapSchema(originalSchema, [ | ||
new RemoveObjectFieldDeprecations('remove this') | ||
]); | ||
|
||
const fields = transformedSchema.getType('Test').getFields(); | ||
expect(fields.first.deprecationReason).toEqual('do not remove'); | ||
expect(fields.second.deprecationReason).toBeUndefined(); | ||
expect(fields.first.astNode.directives.length).toEqual(1); | ||
expect(fields.second.astNode.directives.length).toEqual(0); | ||
}); | ||
|
||
test('removes deprecations by reason regex', async () => { | ||
const transformedSchema = wrapSchema(originalSchema, [ | ||
new RemoveObjectFieldDeprecations(/remove/) | ||
]); | ||
|
||
const fields = transformedSchema.getType('Test').getFields(); | ||
expect(fields.first.deprecationReason).toBeUndefined(); | ||
expect(fields.second.deprecationReason).toBeUndefined(); | ||
expect(fields.first.astNode.directives.length).toEqual(0); | ||
expect(fields.second.astNode.directives.length).toEqual(0); | ||
}); | ||
}); |
66 changes: 66 additions & 0 deletions
66
packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { wrapSchema, RemoveObjectFieldDirectives } from '@graphql-tools/wrap'; | ||
import { makeExecutableSchema } from '@graphql-tools/schema'; | ||
|
||
describe('RemoveObjectFieldDirectives', () => { | ||
const originalSchema = makeExecutableSchema({ | ||
typeDefs: ` | ||
directive @alpha(arg: String) on FIELD_DEFINITION | ||
directive @bravo(arg: String) on FIELD_DEFINITION | ||
type Test { | ||
id: ID! @bravo(arg: "remove this") | ||
first: String! @alpha(arg: "do not remove") | ||
second: String! @alpha(arg: "remove this") | ||
third: String @alpha(arg: "remove this") | ||
} | ||
` | ||
}); | ||
|
||
test('removes directives by name', async () => { | ||
const transformedSchema = wrapSchema(originalSchema, [ | ||
new RemoveObjectFieldDirectives('alpha') | ||
]); | ||
|
||
const fields = transformedSchema.getType('Test').getFields(); | ||
expect(fields.id.astNode.directives.length).toEqual(1); | ||
expect(fields.first.astNode.directives.length).toEqual(0); | ||
expect(fields.second.astNode.directives.length).toEqual(0); | ||
expect(fields.third.astNode.directives.length).toEqual(0); | ||
}); | ||
|
||
test('removes directives by name regex', async () => { | ||
const transformedSchema = wrapSchema(originalSchema, [ | ||
new RemoveObjectFieldDirectives(/^alp/) | ||
]); | ||
|
||
const fields = transformedSchema.getType('Test').getFields(); | ||
expect(fields.id.astNode.directives.length).toEqual(1); | ||
expect(fields.first.astNode.directives.length).toEqual(0); | ||
expect(fields.second.astNode.directives.length).toEqual(0); | ||
expect(fields.third.astNode.directives.length).toEqual(0); | ||
}); | ||
|
||
test('removes directives by argument', async () => { | ||
const transformedSchema = wrapSchema(originalSchema, [ | ||
new RemoveObjectFieldDirectives(/.+/, { arg: 'remove this' }) | ||
]); | ||
|
||
const fields = transformedSchema.getType('Test').getFields(); | ||
expect(fields.id.astNode.directives.length).toEqual(0); | ||
expect(fields.first.astNode.directives.length).toEqual(1); | ||
expect(fields.second.astNode.directives.length).toEqual(0); | ||
expect(fields.third.astNode.directives.length).toEqual(0); | ||
}); | ||
|
||
test('removes directives by argument regex', async () => { | ||
const transformedSchema = wrapSchema(originalSchema, [ | ||
new RemoveObjectFieldDirectives(/.+/, { arg: /remove/ }) | ||
]); | ||
|
||
const fields = transformedSchema.getType('Test').getFields(); | ||
expect(fields.id.astNode.directives.length).toEqual(0); | ||
expect(fields.first.astNode.directives.length).toEqual(0); | ||
expect(fields.second.astNode.directives.length).toEqual(0); | ||
expect(fields.third.astNode.directives.length).toEqual(0); | ||
}); | ||
}); |
Oops, something went wrong.