diff --git a/src/types/validation.ts b/src/types/validation.ts index 557b9c776..798971886 100644 --- a/src/types/validation.ts +++ b/src/types/validation.ts @@ -17,9 +17,12 @@ export type ValidateOnEvent = 'blur' | 'change' | 'input' | 'submit'; export type FormValue = FormValue[] | object | string | number | boolean | null | undefined; -export type FormValidatorFn = ( +export type FormValidatorFn, S extends Form = Form> = ( value: FormValue, - options?: Record + options: T & { + schema?: ResolvedFormSchema | FormSchema; + path: string; + } ) => boolean | Promise; export interface FormValidator { diff --git a/src/validation/schema/__tests__/validateSchema.spec.ts b/src/validation/schema/__tests__/validateSchema.spec.ts index de48173a7..71581b2d9 100644 --- a/src/validation/schema/__tests__/validateSchema.spec.ts +++ b/src/validation/schema/__tests__/validateSchema.spec.ts @@ -4,7 +4,8 @@ import { validateForm, validateFormArray, validateFormField, - validateFormFieldArray + validateFormFieldArray, + validators } from '@inkline/inkline/validation'; import type { FormValidator, ResolvedFormField, ResolvedFormSchema } from '@inkline/inkline'; import { defaultValidationFieldValues, defaultValidationStateValues } from '@inkline/inkline'; @@ -31,7 +32,7 @@ describe('validation', () => { ...defaultValidationFieldValues, ...defaultValidationStateValues, value: 'value', - validators: [{ name: 'required' }] + validators: ['required'] }; const resolvedSchema = await validateFormField(schema); @@ -136,6 +137,32 @@ describe('validation', () => { } ]); }); + + describe('rootSchema', () => { + it('should should provide rootSchema to validator', async () => { + const schema: ResolvedFormField = { + ...defaultValidationFieldValues, + ...defaultValidationStateValues, + value: '', + validators: ['required'] + }; + const rootSchema: ResolvedFormSchema<{ field: string }> = { + ...defaultValidationFieldValues, + ...defaultValidationStateValues, + field: schema + }; + const path = 'field'; + + const requiredValidatorSpy = vi.spyOn(validators, 'required'); + await validateFormField(schema, path, rootSchema); + + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema.value, { + name: schema.validators[0], + path, + schema: rootSchema + }); + }); + }); }); describe('validateFormFieldArray()', () => { @@ -166,6 +193,41 @@ describe('validation', () => { expect(resolvedSchema[1].valid).toEqual(true); expect(resolvedSchema[1].invalid).toEqual(false); }); + + describe('rootSchema', () => { + it('should should pass rootSchema to validateFormFieldSchema', async () => { + const schema = createFormArraySchema([ + { + value: '', + validators: ['required'] + }, + { + value: 'value', + validators: ['required'] + } + ]); + const rootSchema: ResolvedFormSchema<{ array: string[] }> = { + ...defaultValidationFieldValues, + ...defaultValidationStateValues, + array: schema + }; + const path = 'array'; + + const requiredValidatorSpy = vi.spyOn(validators, 'required'); + await validateFormFieldArray(schema, path, rootSchema); + + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema[0].value, { + name: schema[0].validators[0], + path: `${path}.0`, + schema: rootSchema + }); + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema[1].value, { + name: schema[1].validators[0], + path: `${path}.1`, + schema: rootSchema + }); + }); + }); }); describe('validateFormArray()', () => { @@ -200,6 +262,46 @@ describe('validation', () => { expect(resolvedSchema[1].valid).toEqual(true); expect(resolvedSchema[1].invalid).toEqual(false); }); + + describe('rootSchema', () => { + it('should should pass rootSchema to validateFormSchema', async () => { + const schema = createFormArraySchema<{ field: string }>([ + { + field: { + value: '', + validators: ['required'] + } + }, + { + field: { + value: 'value', + validators: ['required'] + } + } + ]) as ResolvedFormSchema<{ field: string }>[]; + + const rootSchema: ResolvedFormSchema<{ array: Array<{ field: string }> }> = { + ...defaultValidationFieldValues, + ...defaultValidationStateValues, + array: schema + }; + const path = 'field'; + + const requiredValidatorSpy = vi.spyOn(validators, 'required'); + await validateFormArray(schema, path, rootSchema); + + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema[0].field.value, { + name: schema[0].field.validators[0], + path: `${path}.0.field`, + schema: rootSchema + }); + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema[1].field.value, { + name: schema[1].field.validators[0], + path: `${path}.1.field`, + schema: rootSchema + }); + }); + }); }); describe('validateForm()', () => { @@ -396,5 +498,163 @@ describe('validation', () => { expect(resolvedSchema.array[1].valid).toEqual(false); expect(resolvedSchema.array[1].field.valid).toEqual(false); }); + + describe('rootSchema', () => { + it('should pass root schema to validateFormField', async () => { + const schema = createSchema<{ + field: string; + }>({ + field: { + value: 'value', + validators: ['required'] + } + }); + const rootSchema: ResolvedFormSchema<{ + group: { + field: string; + }; + }> = { + ...defaultValidationStateValues, + group: schema + }; + + const path = 'group'; + + const requiredValidatorSpy = vi.spyOn(validators, 'required'); + await validateForm(schema, path, rootSchema); + + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema.field.value, { + name: schema.field.validators[0], + path: `${path}.field`, + schema: rootSchema + }); + }); + + it('should pass root schema to validateForm', async () => { + const schema = createSchema<{ + nested: { + field: string; + }; + }>({ + nested: { + field: { + value: 'value', + validators: ['required'] + } + } + }); + const rootSchema: ResolvedFormSchema<{ + group: { + nested: { + field: string; + }; + }; + }> = { + ...defaultValidationStateValues, + group: schema + }; + + const path = 'group'; + + const requiredValidatorSpy = vi.spyOn(validators, 'required'); + await validateForm(schema, path, rootSchema); + + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema.nested.field.value, { + name: schema.nested.field.validators[0], + path: `${path}.nested.field`, + schema: rootSchema + }); + }); + + it('should pass root schema to validateFormFieldArray', async () => { + const schema = createSchema<{ + array: string[]; + }>({ + array: [ + { + value: 'value', + validators: ['required'] + }, + { + value: '', + validators: ['required'] + } + ] + }); + const rootSchema: ResolvedFormSchema<{ + group: { + array: string[]; + }; + }> = { + ...defaultValidationStateValues, + group: schema + }; + + const path = 'group'; + + const requiredValidatorSpy = vi.spyOn(validators, 'required'); + await validateForm(schema, path, rootSchema); + + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema.array[0].value, { + name: schema.array[0].validators[0], + path: `${path}.array.0`, + schema: rootSchema + }); + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema.array[1].value, { + name: schema.array[1].validators[0], + path: `${path}.array.1`, + schema: rootSchema + }); + }); + + it('should pass root schema to validateFormArray', async () => { + const schema = createSchema<{ + array: Array<{ + field: string; + }>; + }>({ + array: [ + { + field: { + value: 'value', + validators: ['required'] + } + }, + { + field: { + value: '', + validators: ['required'] + } + } + ] + }); + const rootSchema: ResolvedFormSchema<{ + group: { + array: Array<{ + field: string; + }>; + }; + }> = { + ...defaultValidationStateValues, + group: schema + }; + + const path = 'group'; + + const requiredValidatorSpy = vi.spyOn(validators, 'required'); + await validateForm(schema, path, rootSchema); + + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema.array[0].field.value, { + name: schema.array[0].field.validators[0], + path: `${path}.array.0.field`, + schema: rootSchema + }); + expect(requiredValidatorSpy).toHaveBeenCalledWith(schema.array[1].field.value, { + name: schema.array[1].field.validators[0], + path: `${path}.array.1.field`, + schema: rootSchema + }); + }); + }); }); }); diff --git a/src/validation/schema/validateSchema.ts b/src/validation/schema/validateSchema.ts index 977de1942..37bb7b45a 100644 --- a/src/validation/schema/validateSchema.ts +++ b/src/validation/schema/validateSchema.ts @@ -8,7 +8,8 @@ import type { Form, FormValue, FormSchema, - FormField + FormField, + FormValidator } from '@inkline/inkline/types'; import { isFormFieldArray, @@ -22,11 +23,13 @@ import { * * @param schema { FormField | ResolvedFormField } * @param path { string } + * @param rootSchema { FormSchema | ResolvedFormSchema } * @returns { ResolvedFormField } */ -export async function validateFormField( +export async function validateFormField( schema: FormField | ResolvedFormField, - path = '' + path = '', + rootSchema?: FormSchema | ResolvedFormSchema ) { const errors: FormError[] = []; const resolvedSchema = { @@ -35,7 +38,14 @@ export async function validateFormField( let valid = true; for (const rawValidator of resolvedSchema.validators || []) { - const validator = typeof rawValidator === 'string' ? { name: rawValidator } : rawValidator; + const validator: FormValidator & { + schema?: FormSchema | ResolvedFormSchema; + path: string; + } = { + ...(typeof rawValidator === 'string' ? { name: rawValidator } : rawValidator), + schema: rootSchema, + path + }; const valueIsValid = await validators[validator.name]( resolvedSchema.value as FormValue, @@ -72,15 +82,17 @@ export async function validateFormField( * * @param schema { FormField[] | ResolvedFormField[] } * @param path { string } + * @param rootSchema { FormSchema | ResolvedFormSchema } * @returns { ResolvedFormField[] } */ -export async function validateFormFieldArray( +export async function validateFormFieldArray( schema: FormField[] | ResolvedFormField[], - path = '' + path = '', + rootSchema?: FormSchema | ResolvedFormSchema ) { return Promise.all( schema.map((item, index) => { - return validateFormField(item, path ? `${path}.${index}` : `${index}`); + return validateFormField(item, path ? `${path}.${index}` : `${index}`, rootSchema); }) ); } @@ -90,15 +102,17 @@ export async function validateFormFieldArray( * * @param schema { FormSchema[] | ResolvedFormSchema[] } * @param path { string } + * @param rootSchema { FormSchema | ResolvedFormSchema } * @returns { Promise[]> } */ -export async function validateFormArray( +export async function validateFormArray( schema: FormSchema[] | ResolvedFormSchema[], - path = '' + path = '', + rootSchema?: FormSchema | ResolvedFormSchema ) { return Promise.all( schema.map((item, index) => { - return validateForm(item, path ? `${path}.${index}` : `${index}`); + return validateForm(item, path ? `${path}.${index}` : `${index}`, rootSchema); }) ); } @@ -108,11 +122,13 @@ export async function validateFormArray( * * @param schema { FormSchema | ResolvedFormSchema } * @param name { string } + * @param rootSchema { FormSchema | ResolvedFormSchema } * @returns { Promise> } */ -export async function validateForm( +export async function validateForm( schema: FormSchema | ResolvedFormSchema, - name = '' + name = '', + rootSchema?: FormSchema | ResolvedFormSchema ) { const resolvedSchema = { ...schema @@ -129,22 +145,26 @@ export async function validateForm( if (isFormFieldArray(field)) { resolvedSchema[key] = (await validateFormFieldArray( field, - name ? `${name}.${key as string}` : `${key as string}` + name ? `${name}.${key as string}` : `${key as string}`, + rootSchema )) as ResolvedFormSchema[keyof T]; } else if (isFormGroupArray(field)) { resolvedSchema[key] = (await validateFormArray( field, - name ? `${name}.${key as string}` : `${key as string}` + name ? `${name}.${key as string}` : `${key as string}`, + rootSchema )) as unknown as ResolvedFormSchema[keyof T]; } else if (isFormField(field)) { resolvedSchema[key] = (await validateFormField( field, - name ? `${name}.${key as string}` : (key as string) + name ? `${name}.${key as string}` : (key as string), + rootSchema )) as ResolvedFormSchema[keyof T]; } else if (isFormGroup(field)) { resolvedSchema[key] = (await validateForm( field as FormSchema, - name ? `${name}.${key as string}` : (key as string) + name ? `${name}.${key as string}` : (key as string), + rootSchema )) as ResolvedFormSchema[keyof T]; } @@ -174,5 +194,5 @@ export async function validateForm( export async function validateSchema( schema: FormSchema | ResolvedFormSchema ) { - return validateForm(schema); + return validateForm(schema, '', schema); } diff --git a/src/validation/validators/__tests__/alpha.spec.ts b/src/validation/validators/__tests__/alpha.spec.ts index 7fbe9e99e..fb0b95d03 100644 --- a/src/validation/validators/__tests__/alpha.spec.ts +++ b/src/validation/validators/__tests__/alpha.spec.ts @@ -2,40 +2,42 @@ import { alpha } from '@inkline/inkline/validation/validators'; describe('Validators', () => { describe('alpha()', () => { + const options = { path: '', schema: undefined }; + it('should return true for lowercase letters', () => { - expect(alpha('abc')).toEqual(true); + expect(alpha('abc', options)).toEqual(true); }); it('should return true for uppercase letters', () => { - expect(alpha('ABC')).toEqual(true); + expect(alpha('ABC', options)).toEqual(true); }); it('should return true for lowercase and uppercase letters', () => { - expect(alpha('abcABC')).toEqual(true); + expect(alpha('abcABC', options)).toEqual(true); }); it('should return true for letters and spaces if options.allowSpace enabled', () => { - expect(alpha('abc ABC', { allowSpaces: true })).toEqual(true); + expect(alpha('abc ABC', { allowSpaces: true, ...options })).toEqual(true); }); it('should return true for letters and dashes if options.allowDash enabled', () => { - expect(alpha('abc-ABC', { allowDashes: true })).toEqual(true); + expect(alpha('abc-ABC', { allowDashes: true, ...options })).toEqual(true); }); it('should return false for letters and numbers', () => { - expect(alpha('abc123')).toEqual(false); + expect(alpha('abc123', options)).toEqual(false); }); it('should return false for letters and symbols', () => { - expect(alpha('abc!')).toEqual(false); + expect(alpha('abc!', options)).toEqual(false); }); it('should return true if all array entries are alpha', () => { - expect(alpha(['abc', 'ABC'])).toEqual(true); + expect(alpha(['abc', 'ABC'], options)).toEqual(true); }); it('should return false if not all array entries are alpha', () => { - expect(alpha(['abc', '123', 'ABC'])).toEqual(false); + expect(alpha(['abc', '123', 'ABC'], options)).toEqual(false); }); }); }); diff --git a/src/validation/validators/__tests__/alphanumeric.spec.ts b/src/validation/validators/__tests__/alphanumeric.spec.ts index 0a7dc9e46..906406601 100644 --- a/src/validation/validators/__tests__/alphanumeric.spec.ts +++ b/src/validation/validators/__tests__/alphanumeric.spec.ts @@ -2,40 +2,42 @@ import { alphanumeric } from '@inkline/inkline/validation/validators'; describe('Validators', () => { describe('alphanumeric()', () => { + const options = { path: '', schema: undefined }; + it('should return true for lowercase letters', () => { - expect(alphanumeric('abc')).toEqual(true); + expect(alphanumeric('abc', options)).toEqual(true); }); it('should return true for uppercase letters', () => { - expect(alphanumeric('ABC')).toEqual(true); + expect(alphanumeric('ABC', options)).toEqual(true); }); it('should return true for lowercase and uppercase letters', () => { - expect(alphanumeric('abcABC')).toEqual(true); + expect(alphanumeric('abcABC', options)).toEqual(true); }); it('should return true for letters and numbers', () => { - expect(alphanumeric('abc123')).toEqual(true); + expect(alphanumeric('abc123', options)).toEqual(true); }); it('should return true for letters, numbers and spaces if options.allowSpace enabled', () => { - expect(alphanumeric('abc ABC123', { allowSpaces: true })).toEqual(true); + expect(alphanumeric('abc ABC123', { allowSpaces: true, ...options })).toEqual(true); }); it('should return true for letters, numbers and dashes if options.allowDash enabled', () => { - expect(alphanumeric('abc-ABC123', { allowDashes: true })).toEqual(true); + expect(alphanumeric('abc-ABC123', { allowDashes: true, ...options })).toEqual(true); }); it('should return false for letters and symbols', () => { - expect(alphanumeric('abc!')).toEqual(false); + expect(alphanumeric('abc!', options)).toEqual(false); }); it('should return true if all array entries are alphanumeric', () => { - expect(alphanumeric(['abc', 'ABC', 'abc123'])).toEqual(true); + expect(alphanumeric(['abc', 'ABC', 'abc123'], options)).toEqual(true); }); it('should return false if not all array entries are alphanumeric', () => { - expect(alphanumeric(['abc', 'a1-23', 'ABC123'])).toEqual(false); + expect(alphanumeric(['abc', 'a1-23', 'ABC123'], options)).toEqual(false); }); }); }); diff --git a/src/validation/validators/__tests__/custom.spec.ts b/src/validation/validators/__tests__/custom.spec.ts index 511f133dd..3eec5a906 100644 --- a/src/validation/validators/__tests__/custom.spec.ts +++ b/src/validation/validators/__tests__/custom.spec.ts @@ -1,32 +1,40 @@ import { custom } from '@inkline/inkline/validation/validators'; +import type { FormValidatorFn, FormValue } from '@inkline/inkline'; describe('Validators', () => { describe('custom()', () => { - const validator = (x: number): boolean => x > 0; - const asyncValidator = (x: number): Promise => Promise.resolve(x > 0); + const options = { path: '', schema: undefined }; + const validator: FormValidatorFn = (x: FormValue): boolean => + typeof x !== 'number' ? false : x > 0; + const asyncValidator: FormValidatorFn = (x: FormValue): Promise => + Promise.resolve(typeof x !== 'number' ? false : x > 0); it('should return true by default', async () => { - expect(await custom(10)).toEqual(true); + expect(await custom(10, options)).toEqual(true); }); it('should apply validator function', async () => { - expect(await custom(10, { validator })).toEqual(true); - expect(await custom(-10, { validator })).toEqual(false); + expect(await custom(10, { validator, ...options })).toEqual(true); + expect(await custom(-10, { validator, ...options })).toEqual(false); }); it('should apply async validator function', async () => { - expect(await custom(10, { validator: asyncValidator })).toEqual(true); - expect(await custom(-10, { validator: asyncValidator })).toEqual(false); + expect(await custom(10, { validator: asyncValidator, ...options })).toEqual(true); + expect(await custom(-10, { validator: asyncValidator, ...options })).toEqual(false); }); it('should apply validator function to every value of array', async () => { - expect(await custom([1, 2, 3], { validator })).toEqual(true); - expect(await custom([0, 1, 2], { validator })).toEqual(false); + expect(await custom([1, 2, 3], { validator, ...options })).toEqual(true); + expect(await custom([0, 1, 2], { validator, ...options })).toEqual(false); }); it('should apply async validator function to every value of array', async () => { - expect(await custom([1, 2, 3], { validator: asyncValidator })).toEqual(true); - expect(await custom([0, 1, 2], { validator: asyncValidator })).toEqual(false); + expect(await custom([1, 2, 3], { validator: asyncValidator, ...options })).toEqual( + true + ); + expect(await custom([0, 1, 2], { validator: asyncValidator, ...options })).toEqual( + false + ); }); }); }); diff --git a/src/validation/validators/__tests__/email.spec.ts b/src/validation/validators/__tests__/email.spec.ts index da38a8600..b8ee12593 100644 --- a/src/validation/validators/__tests__/email.spec.ts +++ b/src/validation/validators/__tests__/email.spec.ts @@ -2,36 +2,38 @@ import { email } from '@inkline/inkline/validation/validators'; describe('Validators', () => { describe('email()', () => { + const options = { path: '', schema: undefined }; + it('should return true for standard email formats', () => { - expect(email('user@example.com')).toEqual(true); + expect(email('user@example.com', options)).toEqual(true); }); it('should return false for missing @', () => { - expect(email('userexample.com')).toEqual(false); + expect(email('userexample.com', options)).toEqual(false); }); it('should return false for missing domain', () => { - expect(email('user@.com')).toEqual(false); + expect(email('user@.com', options)).toEqual(false); }); it('should return false for missing tld', () => { - expect(email('user@example')).toEqual(false); + expect(email('user@example', options)).toEqual(false); }); it('should return true if array contains only valid emails', () => { - expect(email(['user1@example.com', 'user2@example.com'])).toEqual(true); + expect(email(['user1@example.com', 'user2@example.com'], options)).toEqual(true); }); it("should return false if array doesn't contain only valid emails", () => { - expect(email(['user1@example.com', 'user2@example'])).toEqual(false); + expect(email(['user1@example.com', 'user2@example'], options)).toEqual(false); }); it('should return true if value not provided', () => { - expect(email('')).toEqual(true); + expect(email('', options)).toEqual(true); }); it('should return true if array values not provided', () => { - expect(email(['', ''])).toEqual(true); + expect(email(['', ''], options)).toEqual(true); }); }); }); diff --git a/src/validation/validators/__tests__/index.spec.ts b/src/validation/validators/__tests__/index.spec.ts index 9aa27e0b5..4f7fc06fa 100644 --- a/src/validation/validators/__tests__/index.spec.ts +++ b/src/validation/validators/__tests__/index.spec.ts @@ -1,11 +1,11 @@ import { registerValidator, unregisterValidator, validators } from '@inkline/inkline/validation'; -import type { FormValue } from '@inkline/inkline'; +import type { FormValidatorFn } from '@inkline/inkline'; describe('Validators', () => { describe('registerValidator()', () => { it('should register a new validator', () => { const name = 'newValidator'; - const validator = (value: FormValue) => value === 'inkline'; + const validator: FormValidatorFn = (value) => value === 'inkline'; registerValidator(name, validator); @@ -16,7 +16,7 @@ describe('Validators', () => { describe('unregisterValidator()', () => { it('should unregister a validator', () => { const name = 'newValidator'; - const validator = (value: FormValue) => value === 'inkline'; + const validator: FormValidatorFn = (value) => value === 'inkline'; registerValidator(name, validator); unregisterValidator(name); diff --git a/src/validation/validators/__tests__/max.spec.ts b/src/validation/validators/__tests__/max.spec.ts index 603313f35..3d13d8fde 100644 --- a/src/validation/validators/__tests__/max.spec.ts +++ b/src/validation/validators/__tests__/max.spec.ts @@ -2,36 +2,35 @@ import { max } from '@inkline/inkline/validation/validators'; describe('Validators', () => { describe('max()', () => { - it('should return false if value is undefined', () => { - expect(max(undefined, { value: 0 })).toEqual(false); - }); + const options = { path: '', schema: undefined }; - it('should return false if value is null', () => { - expect(max(null, { value: 0 })).toEqual(false); + it('should return false if value is null or undefined', () => { + expect(max(null, { value: 0, ...options })).toEqual(false); + expect(max(undefined, { value: 0, ...options })).toEqual(false); }); it('should return true if value is less than maximum', () => { - expect(max('99', { value: 100 })).toEqual(true); + expect(max('99', { value: 100, ...options })).toEqual(true); }); it('should return true if value is equal to maximum', () => { - expect(max('100', { value: 100 })).toEqual(true); + expect(max('100', { value: 100, ...options })).toEqual(true); }); it('should return false if value is greater than maximum', () => { - expect(max('101', { value: 100 })).toEqual(false); + expect(max('101', { value: 100, ...options })).toEqual(false); }); it('should return true if all array entries are less than maximum', () => { - expect(max(['100', '99', '98'], { value: 100 })).toEqual(true); + expect(max(['100', '99', '98'], { value: 100, ...options })).toEqual(true); }); it('should return false if not all array entries are less than maximum', () => { - expect(max(['101', '100', '99'], { value: 100 })).toEqual(false); + expect(max(['101', '100', '99'], { value: 100, ...options })).toEqual(false); }); - it('should compare to 0 if value not provided', () => { - expect(max(0)).toEqual(true); + it('should pass through if value not provided', () => { + expect(max(0, options)).toEqual(true); }); }); }); diff --git a/src/validation/validators/__tests__/maxLength.spec.ts b/src/validation/validators/__tests__/maxLength.spec.ts index d46ea3e03..3ae0d6fdf 100644 --- a/src/validation/validators/__tests__/maxLength.spec.ts +++ b/src/validation/validators/__tests__/maxLength.spec.ts @@ -2,40 +2,39 @@ import { maxLength } from '@inkline/inkline/validation/validators'; describe('Validators', () => { describe('maxLength()', () => { - it('should return false if value is undefined', () => { - expect(maxLength(undefined, { value: 0 })).toEqual(false); - }); + const options = { path: '', schema: undefined }; - it('should return false if value is null', () => { - expect(maxLength(null, { value: 0 })).toEqual(false); + it('should return false if value is null or undefined', () => { + expect(maxLength(null, { value: 0, ...options })).toEqual(false); + expect(maxLength(undefined, { value: 0, ...options })).toEqual(false); }); it('should return true if string value length is less than maximum', () => { - expect(maxLength('abc', { value: 10 })).toEqual(true); + expect(maxLength('abc', { value: 10, ...options })).toEqual(true); }); it('should return false if string value length is greater than maximum', () => { - expect(maxLength('abc', { value: 1 })).toEqual(false); + expect(maxLength('abc', { value: 1, ...options })).toEqual(false); }); it('should return true if array value length is less than maximum', () => { - expect(maxLength(['a', 'b', 'c'], { value: 10 })).toEqual(true); + expect(maxLength(['a', 'b', 'c'], { value: 10, ...options })).toEqual(true); }); it('should return false if array value length is greater than maximum', () => { - expect(maxLength(['a', 'b', 'c'], { value: 1 })).toEqual(false); + expect(maxLength(['a', 'b', 'c'], { value: 1, ...options })).toEqual(false); }); it('should return true if object value length is less than maximum', () => { - expect(maxLength({ a: 1, b: 2, c: 3 }, { value: 10 })).toEqual(true); + expect(maxLength({ a: 1, b: 2, c: 3 }, { value: 10, ...options })).toEqual(true); }); it('should return false if object value length is greater than maximum', () => { - expect(maxLength({ a: 1, b: 2, c: 3 }, { value: 1 })).toEqual(false); + expect(maxLength({ a: 1, b: 2, c: 3 }, { value: 1, ...options })).toEqual(false); }); - it('should compare to 0 if value not provided', () => { - expect(maxLength({ a: 1, b: 2, c: 3 })).toEqual(false); + it('should pass through if value not provided', () => { + expect(maxLength({ a: 1, b: 2, c: 3 }, options)).toEqual(true); }); }); }); diff --git a/src/validation/validators/__tests__/min.spec.ts b/src/validation/validators/__tests__/min.spec.ts index c9ba56ece..435a78c53 100644 --- a/src/validation/validators/__tests__/min.spec.ts +++ b/src/validation/validators/__tests__/min.spec.ts @@ -2,36 +2,35 @@ import { min } from '@inkline/inkline/validation/validators'; describe('Validators', () => { describe('min()', () => { - it('should return false if value is undefined', () => { - expect(min(undefined, { value: 0 })).toEqual(false); - }); + const options = { path: '', schema: undefined }; - it('should return false if value is null', () => { - expect(min(null, { value: 0 })).toEqual(false); + it('should return false if value is null or undefined', () => { + expect(min(null, { value: 0, ...options })).toEqual(false); + expect(min(undefined, { value: 0, ...options })).toEqual(false); }); it('should return true if value is greater than minimum', () => { - expect(min('1', { value: 0 })).toEqual(true); + expect(min('1', { value: 0, ...options })).toEqual(true); }); it('should return true if value is equal to minimum', () => { - expect(min('0', { value: 0 })).toEqual(true); + expect(min('0', { value: 0, ...options })).toEqual(true); }); it('should return false if value is less than minimum', () => { - expect(min('-1', { value: 0 })).toEqual(false); + expect(min('-1', { value: 0, ...options })).toEqual(false); }); it('should return true if all array entries are greater than minimum', () => { - expect(min(['0', '1', '2'], { value: 0 })).toEqual(true); + expect(min(['0', '1', '2'], { value: 0, ...options })).toEqual(true); }); it('should return false if not all array entries are greater than minimum', () => { - expect(min(['0', '-1', '2'], { value: 0 })).toEqual(false); + expect(min(['0', '-1', '2'], { value: 0, ...options })).toEqual(false); }); - it('should compare to 0 if value not provided', () => { - expect(min(1)).toEqual(true); + it('should pass through if value not provided', () => { + expect(min(1, options)).toEqual(true); }); }); }); diff --git a/src/validation/validators/__tests__/minLength.spec.ts b/src/validation/validators/__tests__/minLength.spec.ts index 2042cf40a..8a02a8d3d 100644 --- a/src/validation/validators/__tests__/minLength.spec.ts +++ b/src/validation/validators/__tests__/minLength.spec.ts @@ -2,40 +2,39 @@ import { minLength } from '@inkline/inkline/validation/validators'; describe('Validators', () => { describe('minLength()', () => { - it('should return false if value is undefined', () => { - expect(minLength(undefined, { value: 0 })).toEqual(false); - }); + const options = { path: '', schema: undefined }; - it('should return false if value is null', () => { - expect(minLength(null, { value: 0 })).toEqual(false); + it('should return false if value is null or undefined', () => { + expect(minLength(null, { value: 0, ...options })).toEqual(false); + expect(minLength(undefined, { value: 0, ...options })).toEqual(false); }); it('should return true if string value length is greater than minimum', () => { - expect(minLength('abc', { value: 0 })).toEqual(true); + expect(minLength('abc', { value: 0, ...options })).toEqual(true); }); it('should return false if string value length is less than minimum', () => { - expect(minLength('abc', { value: 10 })).toEqual(false); + expect(minLength('abc', { value: 10, ...options })).toEqual(false); }); it('should return true if array value length is greater than minimum', () => { - expect(minLength(['a', 'b', 'c'], { value: 0 })).toEqual(true); + expect(minLength(['a', 'b', 'c'], { value: 0, ...options })).toEqual(true); }); it('should return false if array value length is less than minimum', () => { - expect(minLength(['a', 'b', 'c'], { value: 10 })).toEqual(false); + expect(minLength(['a', 'b', 'c'], { value: 10, ...options })).toEqual(false); }); it('should return true if object value length is greater than minimum', () => { - expect(minLength({ a: 1, b: 2, c: 3 }, { value: 0 })).toEqual(true); + expect(minLength({ a: 1, b: 2, c: 3 }, { value: 0, ...options })).toEqual(true); }); it('should return false if object value length is less than minimum', () => { - expect(minLength({ a: 1, b: 2, c: 3 }, { value: 10 })).toEqual(false); + expect(minLength({ a: 1, b: 2, c: 3 }, { value: 10, ...options })).toEqual(false); }); - it('should compare to 0 if value not provided', () => { - expect(minLength({ a: 1, b: 2, c: 3 })).toEqual(true); + it('should pass through if value not provided', () => { + expect(minLength({ a: 1, b: 2, c: 3 }, options)).toEqual(true); }); }); }); diff --git a/src/validation/validators/__tests__/number.spec.ts b/src/validation/validators/__tests__/number.spec.ts index 57ed58617..097a3e1d3 100644 --- a/src/validation/validators/__tests__/number.spec.ts +++ b/src/validation/validators/__tests__/number.spec.ts @@ -2,23 +2,26 @@ import { number } from '@inkline/inkline/validation/validators'; describe('Validators', () => { describe('number()', () => { + const options = { path: '', schema: undefined }; + it('should return true for positive int', () => { - expect(number('10')).toEqual(true); + expect(number('10', options)).toEqual(true); }); it('should return true negative int', () => { - expect(number('-10', { allowNegative: true })).toEqual(true); + expect(number('-10', { allowNegative: true, ...options })).toEqual(true); }); it('should return true for positive float', () => { - expect(number('10.99', { allowDecimal: true })).toEqual(true); + expect(number('10.99', { allowDecimal: true, ...options })).toEqual(true); }); it('should return true negative float', () => { expect( number('-10.99', { allowNegative: true, - allowDecimal: true + allowDecimal: true, + ...options }) ).toEqual(true); }); @@ -27,7 +30,8 @@ describe('Validators', () => { expect( number('-a10.99', { allowNegative: true, - allowDecimal: true + allowDecimal: true, + ...options }) ).toEqual(false); }); @@ -36,7 +40,8 @@ describe('Validators', () => { expect( number(['10', '10.99', '-10', '-10.99'], { allowNegative: true, - allowDecimal: true + allowDecimal: true, + ...options }) ).toEqual(true); }); @@ -45,7 +50,8 @@ describe('Validators', () => { expect( number(['10', '10.99', 'string', '-10', '-10.99'], { allowNegative: true, - allowDecimal: true + allowDecimal: true, + ...options }) ).toEqual(false); }); diff --git a/src/validation/validators/__tests__/required.spec.ts b/src/validation/validators/__tests__/required.spec.ts index 3b80dfb5c..711a86745 100644 --- a/src/validation/validators/__tests__/required.spec.ts +++ b/src/validation/validators/__tests__/required.spec.ts @@ -2,42 +2,44 @@ import { required } from '@inkline/inkline/validation/validators'; describe('Validators', () => { describe('required()', () => { + const options = { path: '', schema: undefined }; + it('should return false if value is undefined', () => { - expect(required(undefined)).toEqual(false); + expect(required(undefined, options)).toEqual(false); }); it('should return false if value is null', () => { - expect(required(null)).toEqual(false); + expect(required(null, options)).toEqual(false); }); it('should return false if value is empty string', () => { - expect(required('')).toEqual(false); + expect(required('', options)).toEqual(false); }); it('should return false if value is blank string', () => { - expect(required(' ')).toEqual(false); + expect(required(' ', options)).toEqual(false); }); it('should return true if value is non-empty string', () => { - expect(required('example')).toEqual(true); + expect(required('example', options)).toEqual(true); }); it('should return true if value is boolean', () => { - expect(required(true)).toEqual(true); - expect(required(false)).toEqual(true); + expect(required(true, options)).toEqual(true); + expect(required(false, options)).toEqual(true); }); it('should return false if value is boolean and false is invalidated', () => { - expect(required(true, { invalidateFalse: true })).toEqual(true); - expect(required(false, { invalidateFalse: true })).toEqual(false); + expect(required(true, { invalidateFalse: true, ...options })).toEqual(true); + expect(required(false, { invalidateFalse: true, ...options })).toEqual(false); }); it('should return false if array is empty', () => { - expect(required([])).toEqual(false); + expect(required([], options)).toEqual(false); }); it('should return true if array is not empty', () => { - expect(required(['a', 'b'])).toEqual(true); + expect(required(['a', 'b'], options)).toEqual(true); }); }); }); diff --git a/src/validation/validators/__tests__/sameAs.spec.ts b/src/validation/validators/__tests__/sameAs.spec.ts index 774f12e92..cba8340f9 100644 --- a/src/validation/validators/__tests__/sameAs.spec.ts +++ b/src/validation/validators/__tests__/sameAs.spec.ts @@ -2,10 +2,13 @@ import { sameAs } from '@inkline/inkline/validation/validators'; describe('Validators', () => { describe('sameAs()', () => { + const options = { path: '' }; + it('should return false if target is not defined', () => { expect( sameAs('', { - schema: () => ({}) + schema: {}, + ...options }) ).toEqual(false); }); @@ -14,7 +17,8 @@ describe('Validators', () => { expect(() => sameAs('', { target: 'target', - schema: () => ({}) + schema: {}, + ...options }) ).toThrow(); }); @@ -23,9 +27,10 @@ describe('Validators', () => { expect( sameAs('', { target: 'target', - schema: () => ({ + schema: { target: { value: 'abc' } - }) + }, + ...options }) ).toEqual(false); }); @@ -34,9 +39,10 @@ describe('Validators', () => { expect( sameAs('abc', { target: 'target', - schema: () => ({ + schema: { target: { value: 'abc' } - }) + }, + ...options }) ).toEqual(true); }); @@ -45,13 +51,14 @@ describe('Validators', () => { expect( sameAs('', { target: 'nested.target', - schema: () => ({ + schema: { nested: { target: { value: 'abc' } } - }) + }, + ...options }) ).toEqual(false); }); @@ -60,19 +67,25 @@ describe('Validators', () => { expect( sameAs('abc', { target: 'nested.target', - schema: () => ({ + schema: { nested: { target: { value: 'abc' } } - }) + }, + ...options }) ).toEqual(true); }); it('should return false if target not provided', () => { - expect(sameAs('abc')).toEqual(false); + expect( + sameAs('abc', { + schema: undefined, + ...options + }) + ).toEqual(false); }); }); }); diff --git a/src/validation/validators/alpha.ts b/src/validation/validators/alpha.ts index b15e43360..071d8aebe 100644 --- a/src/validation/validators/alpha.ts +++ b/src/validation/validators/alpha.ts @@ -1,7 +1,11 @@ import { alpha as validators } from '@inkline/inkline/validation/validators/constants'; -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValidatorFn, FormValue } from '@inkline/inkline/types'; -export function alpha(rawValue: FormValue, options: any = {}): boolean { +export const alpha: FormValidatorFn<{ + locale?: string; + allowDashes?: boolean; + allowSpaces?: boolean; +}> = (rawValue: FormValue, options) => { const locale = options.locale || 'en-US'; const process = (v: FormValue) => { let value = String(v); @@ -21,4 +25,4 @@ export function alpha(rawValue: FormValue, options: any = {}): boolean { } return validators[locale].test(process(rawValue)); -} +}; diff --git a/src/validation/validators/alphanumeric.ts b/src/validation/validators/alphanumeric.ts index 79d1714f0..f5722e37d 100644 --- a/src/validation/validators/alphanumeric.ts +++ b/src/validation/validators/alphanumeric.ts @@ -1,7 +1,11 @@ import { alphanumeric as validators } from '@inkline/inkline/validation/validators/constants'; -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValue, FormValidatorFn } from '@inkline/inkline/types'; -export function alphanumeric(rawValue: FormValue, options: any = {}): boolean { +export const alphanumeric: FormValidatorFn<{ + locale?: string; + allowDashes?: boolean; + allowSpaces?: boolean; +}> = (rawValue: FormValue, options) => { const locale = options.locale || 'en-US'; const process = (v: FormValue) => { let value = String(v); @@ -21,4 +25,4 @@ export function alphanumeric(rawValue: FormValue, options: any = {}): boolean { } return validators[locale].test(process(rawValue)); -} +}; diff --git a/src/validation/validators/custom.ts b/src/validation/validators/custom.ts index 3e1b13cbf..d6fe77c0d 100644 --- a/src/validation/validators/custom.ts +++ b/src/validation/validators/custom.ts @@ -1,9 +1,13 @@ -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValidatorFn, FormValue } from '@inkline/inkline/types'; + +export const custom: FormValidatorFn<{ + validator?: FormValidatorFn; +}> = async (value: FormValue, options) => { + if (!options.validator) { + console.error('No `validator` function provided for custom validator.'); + return true; + } -export async function custom( - value: FormValue, - options: any = { validator: () => true } -): Promise { if (value?.constructor === Array) { let valid = true; for (const v of value) { @@ -13,4 +17,4 @@ export async function custom( } return options.validator(value, options); -} +}; diff --git a/src/validation/validators/email.ts b/src/validation/validators/email.ts index 27aecc206..c3dbf8e16 100644 --- a/src/validation/validators/email.ts +++ b/src/validation/validators/email.ts @@ -1,12 +1,12 @@ -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValidatorFn, FormValue } from '@inkline/inkline/types'; const validator = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -export function email(value: FormValue): boolean { +export const email: FormValidatorFn = (value: FormValue) => { if (value?.constructor === Array) { return value.every((v) => !v || validator.test(String(v))); } return !value || validator.test(String(value)); -} +}; diff --git a/src/validation/validators/max.ts b/src/validation/validators/max.ts index 4d72a1912..1ee61a274 100644 --- a/src/validation/validators/max.ts +++ b/src/validation/validators/max.ts @@ -1,15 +1,20 @@ -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValidatorFn, FormValue } from '@inkline/inkline/types'; -export function max(value: FormValue, options: any = { value: 0 }): boolean { - if (value === undefined || value === null) { +export const max: FormValidatorFn<{ value?: number }> = (value: FormValue, options) => { + if (typeof options.value === 'undefined') { + console.error('The "value" option must be specified for "max" validator.'); + return true; + } + + if (typeof value === 'undefined' || value === null) { return false; } const process = (v: FormValue) => Number(v); if (Array.isArray(value)) { - return value.every((v) => process(v) <= options.value); + return value.every((v) => process(v) <= (options.value as number)); } return process(value) <= options.value; -} +}; diff --git a/src/validation/validators/maxLength.ts b/src/validation/validators/maxLength.ts index d059d36de..0cc653b71 100644 --- a/src/validation/validators/maxLength.ts +++ b/src/validation/validators/maxLength.ts @@ -1,7 +1,12 @@ -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValidatorFn, FormValue } from '@inkline/inkline/types'; -export function maxLength(value: FormValue, options: any = { value: 0 }): boolean { - if (value === undefined || value === null) { +export const maxLength: FormValidatorFn<{ value?: number }> = (value: FormValue, options) => { + if (typeof options.value === 'undefined') { + console.error('The "value" option must be specified for "maxLength" validator.'); + return true; + } + + if (typeof value === 'undefined' || value === null) { return false; } @@ -14,4 +19,4 @@ export function maxLength(value: FormValue, options: any = { value: 0 }): boolea } return String(value).length <= options.value; -} +}; diff --git a/src/validation/validators/min.ts b/src/validation/validators/min.ts index f389baed3..b4462f20d 100644 --- a/src/validation/validators/min.ts +++ b/src/validation/validators/min.ts @@ -1,15 +1,20 @@ -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValue, FormValidatorFn } from '@inkline/inkline/types'; -export function min(value: FormValue, options: any = { value: 0 }): boolean { - if (value === undefined || value === null) { +export const min: FormValidatorFn<{ value?: number }> = (value: FormValue, options) => { + if (typeof options.value === 'undefined') { + console.error('The "value" option must be specified for "min" validator.'); + return true; + } + + if (typeof value === 'undefined' || value === null) { return false; } const process = (v: FormValue) => Number(v); if (Array.isArray(value)) { - return value.every((v) => process(v) >= options.value); + return value.every((v) => process(v) >= (options.value as number)); } return process(value) >= options.value; -} +}; diff --git a/src/validation/validators/minLength.ts b/src/validation/validators/minLength.ts index db894272e..96d46d1f9 100644 --- a/src/validation/validators/minLength.ts +++ b/src/validation/validators/minLength.ts @@ -1,7 +1,12 @@ -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValue, FormValidatorFn } from '@inkline/inkline/types'; -export function minLength(value: FormValue, options: any = { value: 0 }): boolean { - if (value === undefined || value === null) { +export const minLength: FormValidatorFn<{ value?: number }> = (value: FormValue, options) => { + if (typeof options.value === 'undefined') { + console.error('The "value" option must be specified for "minLength" validator.'); + return true; + } + + if (typeof value === 'undefined' || value === null) { return false; } @@ -14,4 +19,4 @@ export function minLength(value: FormValue, options: any = { value: 0 }): boolea } return String(value).length >= options.value; -} +}; diff --git a/src/validation/validators/number.ts b/src/validation/validators/number.ts index fab57f038..a3facf1a3 100644 --- a/src/validation/validators/number.ts +++ b/src/validation/validators/number.ts @@ -1,9 +1,9 @@ -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValidatorFn, FormValue } from '@inkline/inkline/types'; -export function number( +export const number: FormValidatorFn<{ allowNegative?: boolean; allowDecimal?: boolean }> = ( value: FormValue, - options: any = { allowNegative: false, allowDecimal: false } -): boolean { + options +) => { let regExpString = '\\d+'; if (options.allowNegative) { @@ -21,4 +21,4 @@ export function number( } return regExp.test(String(value)); -} +}; diff --git a/src/validation/validators/required.ts b/src/validation/validators/required.ts index 90fcb7c73..baa3f3752 100644 --- a/src/validation/validators/required.ts +++ b/src/validation/validators/required.ts @@ -1,6 +1,9 @@ -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValidatorFn, FormValue } from '@inkline/inkline/types'; -export function required(value: FormValue, options: any = { invalidateFalse: false }): boolean { +export const required: FormValidatorFn<{ invalidateFalse?: boolean }> = ( + value: FormValue, + options +) => { if (value === undefined || value === null) { return false; } @@ -10,9 +13,9 @@ export function required(value: FormValue, options: any = { invalidateFalse: fal } // For checkboxes, false value means unchecked - if (typeof value === typeof true) { + if (typeof value === 'boolean') { return options.invalidateFalse ? !!value : true; } return !!String(value).trim().length; -} +}; diff --git a/src/validation/validators/sameAs.ts b/src/validation/validators/sameAs.ts index 8a700aacf..ea4f9212a 100644 --- a/src/validation/validators/sameAs.ts +++ b/src/validation/validators/sameAs.ts @@ -1,13 +1,15 @@ import { getValueByPath } from '@grozav/utils'; -import type { FormValue } from '@inkline/inkline/types'; +import type { FormValidatorFn, FormValue } from '@inkline/inkline/types'; -export function sameAs(value: FormValue, options: any = {}) { +export const sameAs: FormValidatorFn<{ + target?: string; +}> = (value: FormValue, options) => { if (!options.target) { + console.error('The "target" option must be specified for "sameAs" validator.'); return false; } - const targetSchema = getValueByPath(options.schema(), options.target); - + const targetSchema = getValueByPath(options.schema, options.target); if (!targetSchema) { throw new Error( `Could not find target with name '${options.target}' in 'sameAs' validator.` @@ -15,4 +17,4 @@ export function sameAs(value: FormValue, options: any = {}) { } return value === targetSchema.value; -} +};