-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add
field
method in ValidateTransformer
class, add checks when pa…
…rsing validate directive
- Loading branch information
Showing
7 changed files
with
315 additions
and
20 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
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
159 changes: 154 additions & 5 deletions
159
...fy-graphql-validate-transformer/src/__test__/amplify-graphql-validate-transformer.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 |
---|---|---|
@@ -1,8 +1,157 @@ | ||
import { TransformerContextProvider } from '@aws-amplify/graphql-transformer-interfaces'; | ||
import { testTransform } from '@aws-amplify/graphql-transformer-test-utils'; | ||
import { ModelTransformer } from '@aws-amplify/graphql-model-transformer'; | ||
import { ValidateTransformer } from '..'; | ||
|
||
test('amplify-graphql-validate-transformer', () => { | ||
const transformer = new ValidateTransformer(); | ||
transformer.generateResolvers({} as TransformerContextProvider); | ||
expect(transformer).toBeDefined(); | ||
describe('ValidateTransformer', () => { | ||
/* ================================ */ | ||
/* Valid schema tests */ | ||
/* ================================ */ | ||
it('allows valid validation directives', () => { | ||
const validSchema = /* GraphQL */ ` | ||
type Post @model { | ||
id: ID! | ||
title: String! | ||
@validate(type: minLength, value: "3") | ||
@validate(type: maxLength, value: "10") | ||
rating: Float! | ||
@validate(type: gt, value: "0") | ||
@validate(type: lt, value: "6") | ||
tags: [String]! @validate(type: maxLength, value: "20") | ||
} | ||
`; | ||
|
||
const transformer = new ValidateTransformer(); | ||
expect(() => { | ||
testTransform({ | ||
schema: validSchema, | ||
transformers: [new ModelTransformer(), transformer], | ||
}); | ||
}).not.toThrow(); | ||
}); | ||
|
||
/* ================================ */ | ||
/* Invalid directive usage tests */ | ||
/* ================================ */ | ||
it('throws error if duplicate validation type is used on the same field', () => { | ||
const invalidSchema = /* GraphQL */ ` | ||
type Post @model { | ||
id: ID! | ||
title: String! | ||
@validate(type: minLength, value: "5") | ||
@validate(type: minLength, value: "10") | ||
} | ||
`; | ||
|
||
const transformer = new ValidateTransformer(); | ||
expect(() => { | ||
testTransform({ | ||
schema: invalidSchema, | ||
transformers: [new ModelTransformer(), transformer], | ||
}); | ||
}).toThrow( | ||
"Duplicate @validate directive with type 'minLength' on field 'title'. Each validation type can only be used once per field.", | ||
); | ||
}); | ||
|
||
it('throws error if numeric validation is used on non-numeric field', () => { | ||
const invalidSchema = /* GraphQL */ ` | ||
type Post @model { | ||
id: ID! | ||
title: String! @validate(type: gt, value: "5") | ||
} | ||
`; | ||
|
||
const transformer = new ValidateTransformer(); | ||
expect(() => { | ||
testTransform({ | ||
schema: invalidSchema, | ||
transformers: [new ModelTransformer(), transformer], | ||
}); | ||
}).toThrow("Validation type 'gt' can only be used with numeric fields (Int, Float). Field 'title' is of type 'String'"); | ||
}); | ||
|
||
it('throws error if string validation is used on non-string field', () => { | ||
const invalidSchema = /* GraphQL */ ` | ||
type Post @model { | ||
id: ID! | ||
count: Int! @validate(type: minLength, value: "5") | ||
} | ||
`; | ||
|
||
const transformer = new ValidateTransformer(); | ||
expect(() => { | ||
testTransform({ | ||
schema: invalidSchema, | ||
transformers: [new ModelTransformer(), transformer], | ||
}); | ||
}).toThrow("Validation type 'minLength' can only be used with String fields. Field 'count' is of type 'Int'"); | ||
}); | ||
|
||
// it('throws error if minLength value is not a positive integer', () => { | ||
// const invalidSchema = /* GraphQL */ ` | ||
// type Post @model { | ||
// id: ID! | ||
// title: String! @validate(type: minLength, value: "0") | ||
// } | ||
// `; | ||
|
||
// const transformer = new ValidateTransformer(); | ||
// expect(() => { | ||
// testTransform({ | ||
// schema: invalidSchema, | ||
// transformers: [new ModelTransformer(), transformer], | ||
// }); | ||
// }).toThrow("minLength value must be a positive integer. Received '0' for field 'title'"); | ||
// }); | ||
|
||
it('throws error if maxLength value is a negative integer', () => { | ||
const invalidSchema = /* GraphQL */ ` | ||
type Post @model { | ||
id: ID! | ||
title: String! @validate(type: maxLength, value: "-5") | ||
} | ||
`; | ||
|
||
const transformer = new ValidateTransformer(); | ||
expect(() => { | ||
testTransform({ | ||
schema: invalidSchema, | ||
transformers: [new ModelTransformer(), transformer], | ||
}); | ||
}).toThrow("maxLength value must be a positive integer. Received '-5' for field 'title'"); | ||
}); | ||
|
||
it('throws error if length validation value is not a number', () => { | ||
const invalidSchema = /* GraphQL */ ` | ||
type Post @model { | ||
id: ID! | ||
title: String! @validate(type: minLength, value: "abc") | ||
} | ||
`; | ||
|
||
const transformer = new ValidateTransformer(); | ||
expect(() => { | ||
testTransform({ | ||
schema: invalidSchema, | ||
transformers: [new ModelTransformer(), transformer], | ||
}); | ||
}).toThrow("minLength value must be a positive integer. Received 'abc' for field 'title'"); | ||
}); | ||
|
||
it('throws error if length validation value is a decimal', () => { | ||
const invalidSchema = /* GraphQL */ ` | ||
type Post @model { | ||
id: ID! | ||
title: String! @validate(type: maxLength, value: "5.5") | ||
} | ||
`; | ||
|
||
const transformer = new ValidateTransformer(); | ||
expect(() => { | ||
testTransform({ | ||
schema: invalidSchema, | ||
transformers: [new ModelTransformer(), transformer], | ||
}); | ||
}).toThrow("maxLength value must be a positive integer. Received '5.5' for field 'title'"); | ||
}); | ||
}); |
126 changes: 121 additions & 5 deletions
126
packages/amplify-graphql-validate-transformer/src/graphql-validate-transformer.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 |
---|---|---|
@@ -1,13 +1,129 @@ | ||
import { ValidateDirective } from '@aws-amplify/graphql-directives'; | ||
import { TransformerPluginBase } from '@aws-amplify/graphql-transformer-core'; | ||
import { TransformerContextProvider } from '@aws-amplify/graphql-transformer-interfaces'; | ||
import { TransformerPluginBase, DirectiveWrapper, InvalidDirectiveError } from '@aws-amplify/graphql-transformer-core'; | ||
import { | ||
TransformerContextProvider, | ||
TransformerSchemaVisitStepContextProvider, | ||
TransformerPluginProvider, | ||
} from '@aws-amplify/graphql-transformer-interfaces'; | ||
import { | ||
ArgumentNode, | ||
DirectiveNode, | ||
FieldDefinitionNode, | ||
InterfaceTypeDefinitionNode, | ||
ObjectTypeDefinitionNode, | ||
StringValueNode, | ||
} from 'graphql'; | ||
import { getBaseType } from 'graphql-transformer-common'; | ||
import { NUMERIC_VALIDATION_TYPES, STRING_VALIDATION_TYPES, ValidateArguments, ValidateDirectiveConfiguration } from './types'; | ||
|
||
/** | ||
* Validates that length validation values (minLength, maxLength) are valid positive integers. | ||
*/ | ||
const validateLengthValue = (config: ValidateDirectiveConfiguration): void => { | ||
if (config.type !== 'minLength' && config.type !== 'maxLength') { | ||
return; | ||
} | ||
|
||
const value = parseFloat(config.value); | ||
if (isNaN(value) || !Number.isInteger(value) || value <= 0) { | ||
// TODO: decide if we want to allow 0 | ||
throw new InvalidDirectiveError( | ||
`${config.type} value must be a positive integer. Received '${config.value}' for field '${config.field.name.value}'`, | ||
); | ||
} | ||
}; | ||
|
||
/** | ||
* Validates that the validation type is compatible with the field type. | ||
*/ | ||
const validateTypeCompatibility = (field: FieldDefinitionNode, validationType: string): void => { | ||
const baseTypeName = getBaseType(field.type); | ||
const isNumericValidation = NUMERIC_VALIDATION_TYPES.includes(validationType as any); | ||
const isStringValidation = STRING_VALIDATION_TYPES.includes(validationType as any); | ||
|
||
if (isNumericValidation && baseTypeName !== 'Int' && baseTypeName !== 'Float') { | ||
throw new InvalidDirectiveError( | ||
`Validation type '${validationType}' can only be used with numeric fields (Int, Float). Field '${field.name.value}' is of type '${baseTypeName}'`, | ||
); | ||
} | ||
|
||
if (isStringValidation && baseTypeName !== 'String') { | ||
throw new InvalidDirectiveError( | ||
`Validation type '${validationType}' can only be used with String fields. Field '${field.name.value}' is of type '${baseTypeName}'`, | ||
); | ||
} | ||
}; | ||
|
||
/** | ||
* Validates that there are no duplicate validation types on the same field. | ||
*/ | ||
const validateNoDuplicateTypes = (field: FieldDefinitionNode, currentDirective: DirectiveNode, currentType: string): void => { | ||
for (const peerDirective of field.directives!) { | ||
if (peerDirective === currentDirective) { | ||
continue; | ||
} | ||
|
||
if (peerDirective.name.value === 'validate') { | ||
const peerType = peerDirective.arguments?.find((arg: ArgumentNode) => arg.name.value === 'type')?.value as StringValueNode; | ||
if (peerType?.value === currentType) { | ||
throw new InvalidDirectiveError( | ||
`Duplicate @validate directive with type '${currentType}' on field '${field.name.value}'. Each validation type can only be used once per field.`, | ||
); | ||
} | ||
} | ||
} | ||
}; | ||
|
||
export class ValidateTransformer extends TransformerPluginBase implements TransformerPluginProvider { | ||
private directiveMap = new Map<string, ValidateDirectiveConfiguration[]>(); | ||
|
||
export class ValidateTransformer extends TransformerPluginBase { | ||
constructor() { | ||
super('amplify-graphql-validate-transformer', ValidateDirective.definition); | ||
} | ||
|
||
generateResolvers = (ctx: TransformerContextProvider): void => { | ||
console.log('generateResolvers', ctx); | ||
field = ( | ||
parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, | ||
definition: FieldDefinitionNode, | ||
directive: DirectiveNode, | ||
_: TransformerSchemaVisitStepContextProvider, | ||
): void => { | ||
const directiveWrapped = new DirectiveWrapper(directive); | ||
const config = this.getValidateDirectiveConfiguration(directiveWrapped, parent as ObjectTypeDefinitionNode, definition); | ||
|
||
validateTypeCompatibility(definition, config.type); | ||
validateNoDuplicateTypes(definition, directive, config.type); | ||
validateLengthValue(config); | ||
|
||
if (!this.directiveMap.has(parent.name.value)) { | ||
this.directiveMap.set(parent.name.value, []); | ||
} | ||
this.directiveMap.get(parent.name.value)!.push(config); | ||
}; | ||
|
||
generateResolvers = (_: TransformerContextProvider): void => { | ||
// TODO: | ||
// 1. Generate validation checks in the resolver based on field type | ||
// 2. Return appropriate error messages for validation failures | ||
}; | ||
|
||
private getValidateDirectiveConfiguration( | ||
directive: DirectiveWrapper, | ||
object: ObjectTypeDefinitionNode, | ||
field: FieldDefinitionNode, | ||
): ValidateDirectiveConfiguration { | ||
const defaultArgs: ValidateArguments = { | ||
type: '', | ||
value: '', | ||
errorMessage: '', | ||
}; | ||
const args = directive.getArguments<ValidateArguments>(defaultArgs); | ||
|
||
return { | ||
object, | ||
field, | ||
type: args.type, | ||
value: args.value, | ||
errorMessage: args.errorMessage, | ||
}; | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
packages/amplify-graphql-validate-transformer/src/types.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 { ObjectTypeDefinitionNode, FieldDefinitionNode } from 'graphql'; | ||
|
||
// Validation type groups | ||
export const NUMERIC_VALIDATION_TYPES = ['gt', 'lt', 'gte', 'lte'] as const; | ||
export const STRING_VALIDATION_TYPES = ['minLength', 'maxLength', 'startsWith', 'endsWith', 'matches'] as const; | ||
|
||
// All supported validation types | ||
export const VALIDATION_TYPES = [...NUMERIC_VALIDATION_TYPES, ...STRING_VALIDATION_TYPES] as const; | ||
export type ValidationType = (typeof VALIDATION_TYPES)[number]; | ||
|
||
// Interface for directive arguments | ||
export interface ValidateArguments { | ||
type: ValidationType | ''; | ||
value: string; | ||
errorMessage: string; | ||
} | ||
|
||
// Interface to store validate directive configurations | ||
export interface ValidateDirectiveConfiguration extends ValidateArguments { | ||
object: ObjectTypeDefinitionNode; | ||
field: FieldDefinitionNode; | ||
} |