Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add checks for parsing validate directive #3125

Open
wants to merge 32 commits into
base: feature/server-side-validation
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9656730
add `field` method in `ValidateTransformer` class, add checks when pa…
bobbyu99 Jan 21, 2025
c8ccf7f
rename the test folder so that no duplicate tests are run
bobbyu99 Jan 22, 2025
8b83f69
update snapshot test for repeatable use of validate directive
bobbyu99 Jan 22, 2025
e1f4d7f
ignore types.ts in test coverage
bobbyu99 Jan 22, 2025
c628310
refactor and simplify
bobbyu99 Jan 22, 2025
8210e6f
refactor validator and its tests
bobbyu99 Jan 22, 2025
ee67f15
update API.md and a comment
bobbyu99 Jan 22, 2025
73ca173
used c8 ignore instead of excluding in jest.config.js
bobbyu99 Jan 22, 2025
11161e2
update length value validators and add more edge case tests
bobbyu99 Jan 22, 2025
d280323
add and refactor tests for duplicate validation types
bobbyu99 Jan 22, 2025
3506ca5
refactor type compatibility tests
bobbyu99 Jan 22, 2025
2a8e0c7
update integer validation logic
bobbyu99 Jan 22, 2025
0cbcdfc
prettier
bobbyu99 Jan 22, 2025
310a303
rename test files
bobbyu99 Jan 22, 2025
b5fddfa
refactor tests and add some more test
bobbyu99 Jan 23, 2025
801c31e
fix some bug in tests
bobbyu99 Jan 23, 2025
8dc62d4
prettier
bobbyu99 Jan 23, 2025
714c777
nit
bobbyu99 Jan 23, 2025
873f598
add snapshot tests
bobbyu99 Jan 23, 2025
f389fc7
update types comments to use JSDoc style
bobbyu99 Jan 23, 2025
4a97be8
add numeric validation tests to cover 0, positive & negative ints, po…
bobbyu99 Jan 23, 2025
d21a347
use Number in place of parseInt
bobbyu99 Jan 23, 2025
ba63312
refactor duplicate-validation-types.test.ts
bobbyu99 Jan 23, 2025
4a1b249
refactor minmax-length.test and validation-field-type-compatibility-test
bobbyu99 Jan 24, 2025
8abce32
update snapshot tests
bobbyu99 Jan 24, 2025
f84ca3e
refactor test files
bobbyu99 Jan 24, 2025
3eb7b66
refactor tests files
bobbyu99 Jan 25, 2025
6e642b7
added check and tests for non-model type
bobbyu99 Jan 26, 2025
9101a17
add test description
bobbyu99 Jan 26, 2025
3f8a360
add docs for helper
bobbyu99 Jan 26, 2025
a60fbd6
nit
bobbyu99 Jan 26, 2025
2abf979
add some docs
bobbyu99 Jan 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ Object {
type: ValidationType!
value: String!
errorMessage: String
) on FIELD_DEFINITION
) repeatable on FIELD_DEFINITION

enum ValidationType {
gt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const definition = /* GraphQL */ `
type: ValidationType!
value: String!
errorMessage: String
) on FIELD_DEFINITION
) repeatable on FIELD_DEFINITION

enum ValidationType {
gt
Expand Down
12 changes: 10 additions & 2 deletions packages/amplify-graphql-validate-transformer/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@

```ts

import { DirectiveNode } from 'graphql';
import { FieldDefinitionNode } from 'graphql';
import { InterfaceTypeDefinitionNode } from 'graphql';
import { ObjectTypeDefinitionNode } from 'graphql';
import { TransformerContextProvider } from '@aws-amplify/graphql-transformer-interfaces';
import { TransformerPluginBase } from '@aws-amplify/graphql-transformer-core';
import { TransformerPluginProvider } from '@aws-amplify/graphql-transformer-interfaces';
import { TransformerSchemaVisitStepContextProvider } from '@aws-amplify/graphql-transformer-interfaces';

// @public (undocumented)
export class ValidateTransformer extends TransformerPluginBase {
export class ValidateTransformer extends TransformerPluginBase implements TransformerPluginProvider {
constructor();
// (undocumented)
generateResolvers: (ctx: TransformerContextProvider) => void;
field: (parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, definition: FieldDefinitionNode, directive: DirectiveNode, _: TransformerSchemaVisitStepContextProvider) => void;
// (undocumented)
generateResolvers: (_: TransformerContextProvider) => void;
}

// (No @packageDocumentation comment for this package)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,4 @@ const baseConfig = require('../../jest.config.base.js'); // eslint-disable-line

module.exports = {
...baseConfig,
coverageThreshold: {
global: {
branches: 100,
lines: 100,
functions: 100,
},
},
};
4 changes: 4 additions & 0 deletions packages/amplify-graphql-validate-transformer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,9 @@
"@aws-amplify/graphql-transformer-interfaces": "4.2.1",
"graphql": "^15.5.0",
"graphql-transformer-common": "5.1.2"
},
"devDependencies": {
"@aws-amplify/graphql-transformer-test-utils": "1.0.11",
"@aws-amplify/graphql-model-transformer": "3.1.4"
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { NUMERIC_VALIDATION_TYPES } from '../types';
import {
NUMERIC_FIELD_TYPES,
MIN_MAX_LENGTH_VALIDATION_TYPES,
runTransformTest,
createValidationTestCases,
createValidationSchema,
} from './test-utils';

describe('Input parsing for min/maxLength validations', () => {
describe('Invalid values for min/maxLength validations', () => {
const testInvalidValues = (description: string, values: string[]): void => {
describe(`${description}`, () => {
const testCases = createValidationTestCases([...MIN_MAX_LENGTH_VALIDATION_TYPES], ['String'], values, { fieldName: 'title' });
test.each(testCases)('rejects $validationType value of "$value"', (testCase) => {
const schema = createValidationSchema(testCase);
const error = `${testCase.validationType} value must be a non-negative integer. Received '${testCase.value}' for field 'title'`;
runTransformTest(schema, error);
});
});
};

testInvalidValues('Negative integer values', ['-3', '-10', '-123', '-999999999999999999999999999999']);
testInvalidValues('Negative decimal values', ['-1.23', '-123.4567890', '-353756.38']);
testInvalidValues('Special values', ['NaN', 'undefined', 'null']);
testInvalidValues('Non-numeric length values', ['abc', 'bcdefghijklmnopqrstuvwxyz', '!#>?$O#']);
testInvalidValues('Whitespace values', ['', ' ', ' ']);
});

describe('Valid values for min/maxLength validations', () => {
const testValidValues = (description: string, values: string[]): void => {
describe(`${description}`, () => {
const testCases = createValidationTestCases([...MIN_MAX_LENGTH_VALIDATION_TYPES], ['String'], values, { fieldName: 'title' });
test.each(testCases)('accepts $validationType value of "$value"', (testCase) => {
const schema = createValidationSchema(testCase);
runTransformTest(schema);
});
});
};

testValidValues('Positive integer values', ['3', '10', '123', '1234567890']);
testValidValues('Zero values', ['0', '00', '000', '+0', '-0']);
testValidValues('Large number', ['999999999999999999999999999999']);
});
});

describe('Input parsing for numeric validations', () => {
describe('Invalid values for numeric validations', () => {
const testInvalidValues = (description: string, values: string[]): void => {
describe(`${description}`, () => {
const testCases = createValidationTestCases([...NUMERIC_VALIDATION_TYPES], [...NUMERIC_FIELD_TYPES], values);
test.each(testCases)('rejects `$validationType` validation with value "$value" on `$fieldType` field', (testCase) => {
const schema = createValidationSchema(testCase);
const error = `${testCase.validationType} value must be a number. Received '${testCase.value}' for field 'field'`;
runTransformTest(schema, error);
});
});
};

testInvalidValues('Whitespace values', ['', ' ', ' ']);
testInvalidValues('Special values', ['NaN', 'null', 'undefined']);
});

describe('Valid values for numeric validations', () => {
const testValidValues = (description: string, values: string[]): void => {
describe(`${description}`, () => {
const testCases = createValidationTestCases([...NUMERIC_VALIDATION_TYPES], [...NUMERIC_FIELD_TYPES], values);
test.each(testCases)('accepts `$validationType` validation with value "$value" on `$fieldType` field', (testCase) => {
const schema = createValidationSchema(testCase);
runTransformTest(schema);
});
});
};

testValidValues('Zero values', ['0', '00', '000', '+0', '-0']);
testValidValues('Positive integer values', ['3', '10', '123', '1234567890']);
testValidValues('Positive decimal values', ['1.325', '20.5', '432.123']);
testValidValues('Negative integer values', ['-3', '-10', '-123', '-1234567890']);
testValidValues('Negative decimal values', ['-1.325', '-20.5', '-432.123']);
testValidValues('Infinity', ['Infinity', '-Infinity']);
testValidValues('Extremely large number', ['999999999999999999999999999999']);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { NUMERIC_VALIDATION_TYPES, STRING_VALIDATION_TYPES, VALIDATION_TYPES } from '../types';
import {
NUMERIC_FIELD_TYPES,
STRING_FIELD_TYPES,
ALL_FIELD_TYPES,
runTransformTest,
createValidationTestCases,
createValidationSchema,
} from './test-utils';

describe('Validation on Model vs Non-Model Types', () => {
describe('Disallow validation on non-model type', () => {
describe('Disallow numeric validations on non-model type', () => {
const testCases = createValidationTestCases([...NUMERIC_VALIDATION_TYPES], [...NUMERIC_FIELD_TYPES], ['5']);
test.each(testCases)('rejects `$validationType` validation on `$fieldType` field of non-model type', (testCase) => {
const schema = /* GraphQL */ `
type Post @model {
id: ID!
input: NonModelInput!
}

type NonModelInput {
field: ${testCase.fieldType}! @validate(type: ${testCase.validationType}, value: "${testCase.value}")
}
`;

runTransformTest(schema, '@validate directive can only be used on fields within @model types');
});
});

describe('Disallow string validations on non-model type', () => {
test.each([...STRING_VALIDATION_TYPES])('rejects `%s` validation on `String` field of non-model type', (validationType) => {
const schema = /* GraphQL */ `
type Post @model {
id: ID!
input: NonModelInput!
}

type NonModelInput {
field: String! @validate(type: ${validationType}, value: "test")
}
`;

runTransformTest(schema, '@validate directive can only be used on fields within @model types');
});
});
});

describe('Allow validation on model type', () => {
describe('Allow numeric validations on model type', () => {
const testCases = createValidationTestCases([...NUMERIC_VALIDATION_TYPES], [...NUMERIC_FIELD_TYPES], ['5']);
test.each(testCases)('accepts `$validationType` validation on `$fieldType` field of model type', (testCase) => {
const schema = createValidationSchema(testCase);
runTransformTest(schema);
});
});

describe('Allow string validations on model type', () => {
const testCases = createValidationTestCases([...STRING_VALIDATION_TYPES], [...STRING_FIELD_TYPES], ['5']);
test.each(testCases)('accepts `$validationType` validation on `String` field of model type', (testCase) => {
const schema = createValidationSchema(testCase);
runTransformTest(schema);
});
});
});
});

describe('Disallow validation on list fields', () => {
const testCases = createValidationTestCases([...VALIDATION_TYPES], [...ALL_FIELD_TYPES], ['test', '0']);
test.each(testCases)('rejects `$validationType` validation on list of `$fieldType` field', (testCase) => {
const schema = createValidationSchema({
...testCase,
fieldType: `[${testCase.fieldType}]`,
});
runTransformTest(schema, "@validate directive cannot be used on list field 'field'");
});
});

describe('Duplicate Validation Types on the same field', () => {
describe('Disallow duplicate validation types on the same field', () => {
describe('Disallow duplicate numeric validations on the same field', () => {
const testCases = createValidationTestCases([...NUMERIC_VALIDATION_TYPES], [...NUMERIC_FIELD_TYPES], ['0'], { fieldName: 'rating' });
test.each(testCases)('rejects duplicate `$validationType` validation on `$fieldType` field', (testCase) => {
const schema = /* GraphQL */ `
type Post @model {
id: ID!
rating: ${testCase.fieldType}! @validate(type: ${testCase.validationType}, value: "0") @validate(type: ${testCase.validationType}, value: "1")
}
`;
const error = `Duplicate @validate directive with type '${testCase.validationType}' on field 'rating'. Each validation type can only be used once per field.`;

runTransformTest(schema, error);
});
});

describe('Disallow duplicate string validations on the same field', () => {
const testValues = {
minLength: ['5', '10'],
maxLength: ['5', '10'],
startsWith: ['prefix1', 'prefix2'],
endsWith: ['suffix1', 'suffix2'],
matches: ['regex1', 'regex2'],
};

const testCases = [...STRING_VALIDATION_TYPES].map((type) => ({
type,
values: testValues[type],
}));

test.each(testCases)('rejects duplicate `$type` validation on `String` field', ({ type, values }) => {
const schema = /* GraphQL */ `
type Post @model {
id: ID!
title: String! @validate(type: ${type}, value: "${values[0]}") @validate(type: ${type}, value: "${values[1]}")
}
`;
const error = `Duplicate @validate directive with type '${type}' on field 'title'. Each validation type can only be used once per field.`;

runTransformTest(schema, error);
});
});
});

describe('Allow non-duplicate validation types on the same field', () => {
test.each([
{
name: 'accepts different validation types on same field',
schema: /* GraphQL */ `
type Post @model {
id: ID!
title: String! @validate(type: minLength, value: "5") @validate(type: maxLength, value: "10")
rating: Float! @validate(type: gt, value: "0") @validate(type: lt, value: "6")
score: Int! @validate(type: gte, value: "10") @validate(type: lte, value: "20")
description: String!
@validate(type: startsWith, value: "prefix")
@validate(type: endsWith, value: "suffix")
@validate(type: matches, value: "regex")
}
`,
},
])('$name', ({ schema }) => {
runTransformTest(schema);
});
});
});

describe('Validation Type Compatibility with Field Type', () => {
describe('Disallow validation on incompatible fields', () => {
type FieldType = {
type: string;
value: string;
isObject?: boolean;
};

const fieldTypes: FieldType[] = [
{ type: 'String', value: 'test' },
{ type: 'ID', value: 'test-id' },
{ type: 'Boolean', value: 'true' },
{ type: 'Int', value: '5' },
{ type: 'Float', value: '5.0' },
{ type: 'Author', value: 'author-123', isObject: true },
];

const testInvalidFieldTypes = (
validationTypes: string[],
allowedTypes: string[],
errorMessageFn: (type: string, field: FieldType) => string,
): void => {
test.each(
validationTypes.flatMap((validationType) =>
fieldTypes
.filter((fieldType) => !allowedTypes.includes(fieldType.type))
.map((fieldType) => ({
validationType,
fieldType,
})),
),
)('rejects `$validationType` validation on `$fieldType.type` field', ({ validationType, fieldType }) => {
const schema = createValidationSchema(
{ validationType, fieldType: fieldType.type, value: fieldType.value },
fieldType.isObject
? /* GraphQL */ `
type Author @model {
id: ID!
name: String!
}
`
: undefined,
);
runTransformTest(schema, errorMessageFn(validationType, fieldType));
});
};

describe('Disallow numeric validations on non-numeric fields', () => {
testInvalidFieldTypes(
[...NUMERIC_VALIDATION_TYPES],
[...NUMERIC_FIELD_TYPES],
(validationType, fieldType) =>
`Validation type '${validationType}' can only be used with numeric fields (Int, Float). Field 'field' is of type '${fieldType.type}'`,
);
});

describe('Disallow string validations on non-string fields', () => {
testInvalidFieldTypes(
[...STRING_VALIDATION_TYPES],
[...STRING_FIELD_TYPES],
(validationType, fieldType) =>
`Validation type '${validationType}' can only be used with 'String' fields. Field 'field' is of type '${fieldType.type}'`,
);
});
});

describe('Allow validations on compatible fields', () => {
describe('Allow numeric validations on numeric fields', () => {
const testCases = createValidationTestCases([...NUMERIC_VALIDATION_TYPES], [...NUMERIC_FIELD_TYPES], ['100']);
test.each(testCases)('accepts `$validationType` validation on `$fieldType` field with value `$value`', (testCase) => {
const schema = createValidationSchema(testCase);
runTransformTest(schema);
});
});

describe('Allow string validations on string fields', () => {
const testCases = createValidationTestCases([...STRING_VALIDATION_TYPES], [...STRING_FIELD_TYPES], ['5']);
test.each(testCases)('accepts `$validationType` validation on `$fieldType` field with value `$value`', (testCase) => {
const schema = createValidationSchema(testCase);
runTransformTest(schema);
});
});
});
});
Loading
Loading