diff --git a/index.d.ts b/index.d.ts index 19fbe6f4..3ff19007 100644 --- a/index.d.ts +++ b/index.d.ts @@ -24,7 +24,7 @@ export type ValidationRuleName = | "tuple" | "url" | "uuid" - | string; + | (string & {}); /** * Validation schema definition for "any" built-in validator @@ -42,43 +42,43 @@ export interface RuleAny extends RuleCustom { * @see https://github.com/icebob/fastest-validator#array */ export interface RuleArray extends RuleCustom { - /** - * Name of built-in validator - */ - type: "array"; - /** - * If true, the validator accepts an empty array []. - * @default true - */ - empty?: boolean; - /** - * Minimum count of elements - */ - min?: number; - /** - * Maximum count of elements - */ - max?: number; - /** - * Fixed count of elements - */ - length?: number; - /** - * The array must contain this element too - */ - contains?: T | T[]; + /** + * Name of built-in validator + */ + type: "array"; + /** + * If true, the validator accepts an empty array []. + * @default true + */ + empty?: boolean; + /** + * Minimum count of elements + */ + min?: number; + /** + * Maximum count of elements + */ + max?: number; + /** + * Fixed count of elements + */ + length?: number; + /** + * The array must contain this element too + */ + contains?: T | T[]; /** * The array must be unique (array of objects is always unique). */ unique?: boolean; - /** - * Every element must be an element of the enum array - */ - enum?: T[]; - /** - * Validation rules that should be applied to each element of array - */ - items?: ValidationRule; + /** + * Every element must be an element of the enum array + */ + enum?: T[]; + /** + * Validation rules that should be applied to each element of array + */ + items?: ValidationRule; /** * Wrap value into array if different type provided */ @@ -853,7 +853,9 @@ export interface BuiltInMessages { /** * Type with description of custom error messages */ -export type MessagesType = BuiltInMessages & { [key: string]: string }; +export type MessagesType = BuiltInMessages & { + [key: string]: string | undefined; +}; /** * Union type of all possible built-in validators @@ -890,8 +892,8 @@ export type ValidationRuleObject = */ export type ValidationRule = | ValidationRuleObject - | ValidationRuleObject[] - | ValidationRuleName; + | ValidationRuleName + | (ValidationRuleObject | ValidationRuleName)[]; /** * @@ -921,13 +923,24 @@ export interface ValidationSchemaMetaKeys { /** * Definition for validation schema based on validation rules */ -export type ValidationSchema = ValidationSchemaMetaKeys & { +export type ValidationSchema = + | ({ $$root?: false } & ValidationSchemaNested & + ValidationSchemaMetaKeys) + // If $$root is true, we expect a ValidationRuleObject, not a JS object schema. + | ({ $$root: true } & ValidationRuleObject & ValidationSchemaMetaKeys) + // If it's not set + | ({ $$root?: boolean } & (ValidationRule | ValidationSchemaNested) & + ValidationSchemaMetaKeys); + +export type ValidationSchemaNested = { /** - * List of validation rules for each defined field + * List of validation rules for each defined field. + * Note that `boolean` is only acceptable for ValidationSchemaMetaKeys. + * However, omitting it here would cause TypeScript errors. + * @see https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures */ - [key in keyof T]: ValidationRule | undefined; -} - + [key in keyof T]: ValidationRule | boolean | undefined; +}; /** * Structure with description of validation error message @@ -1022,7 +1035,7 @@ export interface Context { customs: { [ruleName: string]: { schema: RuleCustom; messages: MessagesType }; }; - meta?: object; + meta?: Record; data: DATA; } @@ -1065,17 +1078,19 @@ export interface CheckFunctionOptions { meta?: object | null; } -export interface SyncCheckFunction { - (value: any, opts?: CheckFunctionOptions): true | ValidationError[] +export interface SyncCheckFunction { + (value: T, opts?: CheckFunctionOptions): true | ValidationError[] async: false } -export interface AsyncCheckFunction { - (value: any, opts?: CheckFunctionOptions): Promise +export interface AsyncCheckFunction { + (value: T, opts?: CheckFunctionOptions): Promise async: true } -export default class Validator { +export default class Validator< + VCO extends ValidatorConstructorOptions = ValidatorConstructorOptions, +> { /** * List of possible error messages */ @@ -1095,7 +1110,7 @@ export default class Validator { * Constructor of validation class * @param {ValidatorConstructorOptions} opts List of possible validator constructor options */ - constructor(opts?: ValidatorConstructorOptions); + constructor(opts?: VCO); /** * Register a custom validation rule in validation object @@ -1145,13 +1160,19 @@ export default class Validator { }): string; /** - * Compile validator functions that working up 100 times faster that native validation process + * Compile validator functions that works up to 100 times faster that a native validation process * @param {ValidationSchema | ValidationSchema[]} schema Validation schema definition that should be used for validation * @return {(value: any) => (true | ValidationError[])} function that can be used next for validation of current schema */ - compile( - schema: ValidationSchema | ValidationSchema[] - ): SyncCheckFunction | AsyncCheckFunction; + compile< + VS extends ValidationSchema, + CompiledType = + | TypeFromValidationSchema + // We don't do type inference for `considerNullAsAValue`, to keep it simple. + | (VCO extends { considerNullAsAValue: true } ? any : never), + >( + schema: VS, + ): SyncCheckFunction | AsyncCheckFunction; /** * Native validation method to validate obj @@ -1159,9 +1180,9 @@ export default class Validator { * @param {ValidationSchema} schema Validation schema definition that should be used for validation * @return {{true} | ValidationError[]} */ - validate( - value: any, - schema: ValidationSchema + validate( + value: TypeFromValidationSchema, + schema: VS, ): true | ValidationError[] | Promise; /** @@ -1190,3 +1211,224 @@ export default class Validator { value: ValidationSchema | string | any ): ValidationRule | ValidationSchema } + +/* + * + * INFERENCE TYPES + * + */ + +type TypeFromAnySchema< + Schema extends + | ValidationRuleName + | ValidationRuleObject + | ValidationRuleObject[] + | ValidationSchema + | ValidationSchema[], +> = Schema extends ValidationRule + ? TypeFromValidationRule + : // Basic ValidationSchema? + Schema extends ValidationSchema + ? TypeFromValidationSchema + : // ValidationSchema array? We take the union of each entry. + Schema extends ValidationSchema[] + ? { [K in number]: TypeFromValidationSchema }[number] + : never; + +/** + * Infers the TS type from a @see ValidationSchema + * E.g. + * ```ts + * TypeFromValidationSchema<{ param1: {type: "string"}, param2: "number" }> + * // Infers type { param1: string, param2: number} + * TypeFromValidationSchema<{ $$root: true, type: "multi", rules: ["string", "boolean"] }> + * // Infers type `string | boolean` + * ``` + */ +export type TypeFromValidationSchema = + // Schema is a validation rule? + | (VS extends ValidationRule + ? TypeFromValidationRule + : // Schema is an object schema. + Optionalize<{ + [Param in Exclude< + keyof VS, + keyof ValidationSchemaMetaKeys + // @ts-ignore + >]: TypeFromValidationRule; + }> & + object) + // We can't check type if one of the properties is of type `equal`. + | AnyIfHasTypeEqual; + +/** + * Infers the TS type from a @see ValidationRule + * + * E.g. + * - `{type: "string", default: "Bob"}` returns type `string | undefined` + * - `"number"` returns type `number` + * - `[{type: "string"}, {type: "number"}]` returns type `string | number` + * - `{ type: "array", items: "string"}` returns type `string[]` + */ +export type TypeFromValidationRule = + VR extends ValidationRuleObject + ? TypeFromValidationRuleObject + : // VR is a string that is present in type name->type map? + VR extends keyof BasicValidatorTypeMap + ? BasicValidatorTypeMap[VR] + : // Array of ValidationRuleObjects. + VR extends ValidationRule[] + ? { [K in number]: TypeFromValidationRule }[number] + : // None of the above... + any; + +/** + * Infers the TS type from a ValidationRuleObject + * + * E.g. + * - `{ type: "number", default: 2}` returns type `number | undefined`. + * - `{ type: "array", items: "string"}` returns type `string[]` + */ +type TypeFromValidationRuleObject = + // ==== Infer the basic type ==== + | IntersectIfT2NotNever< + VRO extends { convert: true } + ? // Special case: convert is activated (to boolean or array). + TypeFromConvertValidationRule + : // Base type inferred from the `type` property + TypeFromValidationRuleInner, + // Allow additional object values if `strict` is not true. + VRO["type"] extends "object" + ? VRO extends { strict: true } + ? never + : Record + : never + > + // ==== Optional, default, nullable types ==== + // Include the type of `default` if it exists + | ( + | (VRO extends { default: infer D } + ? (D & {}) | undefined | null + : never) + + // Allow `undefined` and `null` if `optional` is true. + | (VRO extends { optional: true } ? undefined | null : never) + + // Include `null` if `optional` is true + | (VRO extends { nullable: true } ? null : never) + + // Allow any value if `remove` is true (in case of type `forbidden`). + | (VRO extends { remove: true } ? any : never) + ); + +type TypeFromValidationRuleInner = + VRO["type"] extends keyof BasicValidatorTypeMap + ? BasicValidatorTypeMap[VRO["type"]] + : // Type array? + VRO["type"] extends "array" + ? Array> + : // Type multi (union type)? + VRO["type"] extends "multi" + ? MultiType + : // Type object? + VRO["type"] extends "object" + ? TypeFromRuleObject + : // Class instance? + VRO["type"] extends "class" + ? InstanceType + : VRO["type"] extends "enum" + ? VRO["values"][number] + : // None of the above... + any; // Not covered: Tuples, typed Record values + +type TypeFromConvertValidationRule = + // Allow converting to boolean + VRO["type"] extends "boolean" + ? "true" | "false" | 1 | 0 | "on" | "off" | true | false + : // Allow converting string and timestamps to date + VRO["type"] extends "date" + ? Date | string | number + : // Allow converting anything to number. + VRO["type"] extends "number" + ? any + : // Allow converting single value to array + VRO["type"] extends "array" + ? + | TypeFromValidationRule + | Array> + : VRO["type"] extends "objectID" + ? any + : VRO["type"] extends "string" + ? any + : never; + +/** Fastest-validator types with primitive mapping. */ +type BasicValidatorTypeMap = { + any: any; + boolean: boolean; + currency: string; + custom: any; + date: Date | string; + email: string; + equal: any; + forbidden: null | undefined; + function: Function; + luhn: string; + mac: string; + number: number; + objectID: any; + record: Record; + string: string; + tuple: any[]; + url: string; + uuid: string; +}; + +type TypeFromRuleObject = + VRO["props"] extends object + ? TypeFromValidationSchema + : VRO["properties"] extends object + ? TypeFromValidationSchema + : object; + +/** + * Infers schema definitions from an array of schema properties ("multitype") into one type. + * **Attention**: Using multi with more than one rule of type object fails. + */ +type MultiType< + ParameterSchemas extends (ValidationRuleObject | ValidationRuleName)[] = [], +> = { + [Index in keyof ParameterSchemas]: TypeFromValidationRule< + ParameterSchemas[Index] + >; +}[number]; + +/** + * A helper type that takes an object and makes properties optional + * if their type includes `undefined`. + * + * For example, for `{ a: string | undefined, b: string }`, it returns + * `{ a?: string | undefined, b: string }`. + */ +type Optionalize = { + // Pick optional properties and make them optional + [K in keyof T as undefined extends T[K] ? K : never]?: T[K]; +} & { + // Pick required properties + [K in keyof T as undefined extends T[K] ? never : K]: T[K]; +}; + +/** + * Helper type to intersect `T1 & T2` + * if T2 is not `never`. + */ +type IntersectIfT2NotNever = + ExtendsNever extends true ? T1 : T1 & T2; + +type ExtendsNever = [T] extends [never] ? true : false; + +type AnyIfHasTypeEqual = { + [Param in keyof Schema]: Schema[Param] extends { type: "equal" } + ? any + : never; +}[keyof Schema]; diff --git a/test/typescript/integration.spec.ts b/test/typescript/integration.spec.ts index e4a1cdb1..86337d26 100644 --- a/test/typescript/integration.spec.ts +++ b/test/typescript/integration.spec.ts @@ -1,4 +1,4 @@ -import Validator from '../../'; +import Validator, { ValidationSchema, ValidationRule } from '../../'; describe('TypeScript Definitions', () => { describe('Test flat schema', () => { @@ -57,7 +57,7 @@ describe('TypeScript Definitions', () => { zip: { type: 'number', min: 100, max: 99999 }, }, }, - }; + } as const; let check = v.compile(schema); it('should give true if obj is valid', () => { @@ -86,6 +86,7 @@ describe('TypeScript Definitions', () => { }, }; + // @ts-expect-error let res = check(obj); expect(res).toBeInstanceOf(Array); @@ -713,10 +714,11 @@ describe('TypeScript Definitions', () => { type: 'array', items: 'number', }, - ]; + ] satisfies ValidationRule; + const a:ValidationSchema = schema; let check = v.compile(schema); - + it('should give true if first array is given', () => { let obj = ['hello', 'there', 'this', 'is', 'a', 'test']; @@ -736,6 +738,7 @@ describe('TypeScript Definitions', () => { it('should give error if the array is broken', () => { let obj = ['hello', 3]; + // @ts-expect-error let res = check(obj); expect(res).toBeInstanceOf(Array); @@ -749,6 +752,7 @@ describe('TypeScript Definitions', () => { it('should give error if the array is broken', () => { let obj = [true, false]; + // @ts-expect-error let res = check(obj); expect(res).toBeInstanceOf(Array); @@ -769,7 +773,7 @@ describe('TypeScript Definitions', () => { it('should compile and validate', () => { const schema = { valid: { type: 'object' }, - }; + } satisfies ValidationSchema; const check = v.compile(schema); expect(check).toBeInstanceOf(Function); @@ -785,7 +789,7 @@ describe('TypeScript Definitions', () => { it('should compile and validate', () => { const schema = { valid: { type: 'array' }, - }; + } satisfies ValidationSchema; const check = v.compile(schema); expect(check).toBeInstanceOf(Function); @@ -927,7 +931,8 @@ describe('TypeScript Definitions', () => { let schema = { name: 'string', $$strict: true, - }; + } satisfies ValidationSchema; + let check = v.compile(schema); @@ -962,7 +967,7 @@ describe('TypeScript Definitions', () => { }, }, $$strict: true, - }; + } satisfies ValidationSchema; let check = v.compile(schema); @@ -999,7 +1004,7 @@ describe('TypeScript Definitions', () => { street: 'string', }, }, - }; + } satisfies ValidationSchema; let check = v.compile(schema); @@ -1031,7 +1036,7 @@ describe('TypeScript Definitions', () => { status: { type: 'boolean', default: true }, tuple: { type: 'tuple', items: [{ type: 'number', default: 666 }, { type: 'string', default: 'lucifer' }] }, array: { type: 'array', items: { type: 'string', default: 'bar' } }, - }; + } satisfies ValidationSchema; let check = v.compile(schema); it('should fill not defined properties', () => { @@ -1071,7 +1076,7 @@ describe('TypeScript Definitions', () => { { type: "number", optional: true }, ], }, - }; + } satisfies ValidationSchema; const check = v.compile(schema); expect(check({})).toBe(true); @@ -1083,7 +1088,7 @@ describe('TypeScript Definitions', () => { }); it("should not throw error if value is null", () => { - const schema = { foo: { type: "number", optional: true } }; + const schema = { foo: { type: "number", optional: true } } satisfies ValidationSchema; const check = v.compile(schema); const o = { foo: null, array: [null], tuple: [null] }; @@ -1092,7 +1097,7 @@ describe('TypeScript Definitions', () => { }); it("should not throw error if value exist", () => { - const schema = { foo: { type: "number", optional: true } }; + const schema = { foo: { type: "number", optional: true } } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 2 })).toBe(true); @@ -1109,8 +1114,7 @@ describe('TypeScript Definitions', () => { { type: "number", optional: true, default: 666 }, ], }, - - }; + } satisfies ValidationSchema; const check = v.compile(schema); const o1 = { foo: 2, array: [], tuple: [6] }; @@ -1132,15 +1136,17 @@ describe('TypeScript Definitions', () => { const v = new Validator(); it("should throw error if value is undefined", () => { - const schema = { foo: { type: "number", nullable: true } }; + const schema = { foo: { type: "number", nullable: true } } satisfies ValidationSchema; const check = v.compile(schema); + // @ts-expect-error expect(check(check)).toBeInstanceOf(Array); + // @ts-expect-error expect(check({ foo: undefined })).toBeInstanceOf(Array); }); it("should not throw error if value is null", () => { - const schema = { foo: { type: "number", nullable: true } }; + const schema = { foo: { type: "number", nullable: true } } satisfies ValidationSchema; const check = v.compile(schema); const o = { foo: null }; @@ -1149,13 +1155,13 @@ describe('TypeScript Definitions', () => { }); it("should not throw error if value exist", () => { - const schema = { foo: { type: "number", nullable: true } }; + const schema = { foo: { type: "number", nullable: true } } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 2 })).toBe(true); }); it("should set default value if there is a default", () => { - const schema = { foo: { type: "number", nullable: true, default: 5 } }; + const schema = { foo: { type: "number", nullable: true, default: 5 } } satisfies ValidationSchema; const check = v.compile(schema); const o1 = { foo: 2 }; @@ -1168,7 +1174,7 @@ describe('TypeScript Definitions', () => { }); it("should not set default value if current value is null", () => { - const schema = { foo: { type: "number", nullable: true, default: 5 } }; + const schema = { foo: { type: "number", nullable: true, default: 5 } } satisfies ValidationSchema; const check = v.compile(schema); const o = { foo: null }; @@ -1177,7 +1183,7 @@ describe('TypeScript Definitions', () => { }); it("should work with optional", () => { - const schema = { foo: { type: "number", nullable: true, optional: true } }; + const schema = { foo: { type: "number", nullable: true, optional: true } } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 3 })).toBe(true); @@ -1186,7 +1192,7 @@ describe('TypeScript Definitions', () => { }); it("should work with optional and default", () => { - const schema = { foo: { type: "number", nullable: true, optional: true, default: 5 } }; + const schema = { foo: { type: "number", nullable: true, optional: true, default: 5 } } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 3 })).toBe(true); @@ -1201,7 +1207,7 @@ describe('TypeScript Definitions', () => { }); it("should accept null value when optional", () => { - const schema = { foo: { type: "number", nullable: false, optional: true } }; + const schema = { foo: { type: "number", nullable: false, optional: true } } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 3 })).toBe(true); @@ -1211,22 +1217,27 @@ describe('TypeScript Definitions', () => { }); it("should accept null as value when required", () => { - const schema = {foo: {type: "number", nullable: true, optional: false}}; + const schema = {foo: {type: "number", nullable: true, optional: false}} satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 3 })).toBe(true); + // @ts-expect-error expect(check({ foo: undefined })).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]); + // @ts-expect-error expect(check({})).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]); expect(check({ foo: null })).toBe(true); }); it("should not accept null as value when required and not explicitly not nullable", () => { - const schema = {foo: {type: "number", optional: false}}; + const schema = {foo: {type: "number", optional: false}} satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 3 })).toBe(true); + // @ts-expect-error expect(check({ foo: undefined })).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]); + // @ts-expect-error expect(check({})).toEqual([{"actual": undefined, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]); + // @ts-expect-error expect(check({ foo: null })).toEqual([{"actual": null, "field": "foo", "message": "The 'foo' field is required.", "type": "required"}]); }); }); @@ -1235,7 +1246,7 @@ describe('TypeScript Definitions', () => { const v = new Validator({considerNullAsAValue: true}); it("should throw error if value is undefined", () => { - const schema = { foo: { type: "number" } }; + const schema = { foo: { type: "number" } } satisfies ValidationSchema; const check = v.compile(schema); expect(check(check)).toBeInstanceOf(Array); @@ -1243,7 +1254,7 @@ describe('TypeScript Definitions', () => { }); it("should not throw error if value is null", () => { - const schema = { foo: { type: "number" } }; + const schema = { foo: { type: "number" } } satisfies ValidationSchema; const check = v.compile(schema); const o = { foo: null }; @@ -1252,13 +1263,13 @@ describe('TypeScript Definitions', () => { }); it("should not throw error if value exist", () => { - const schema = { foo: { type: "number" } }; + const schema = { foo: { type: "number" } } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 2 })).toBe(true); }); it("should set default value if there is a default", () => { - const schema = { foo: { type: "number", default: 5 } }; + const schema = { foo: { type: "number", default: 5 } } satisfies ValidationSchema; const check = v.compile(schema); const o1 = { foo: 2 }; @@ -1271,7 +1282,7 @@ describe('TypeScript Definitions', () => { }); it("should not set default value if current value is null", () => { - const schema = { foo: { type: "number", default: 5 } }; + const schema = { foo: { type: "number", default: 5 } } satisfies ValidationSchema; const check = v.compile(schema); const o = { foo: null }; @@ -1280,7 +1291,7 @@ describe('TypeScript Definitions', () => { }); it("should set default value if current value is null but can't be", () => { - const schema = { foo: { type: "number", default: 5, nullable: false } }; + const schema = { foo: { type: "number", default: 5, nullable: false } } satisfies ValidationSchema; const check = v.compile(schema); const o = { foo: null }; @@ -1289,7 +1300,7 @@ describe('TypeScript Definitions', () => { }); it("should set default value if current value is null but optional", () => { - const schema = { foo: { type: "number", default: 5, nullable: false, optional: true } }; + const schema = { foo: { type: "number", default: 5, nullable: false, optional: true } } satisfies ValidationSchema; const check = v.compile(schema); const o = { foo: null }; @@ -1298,7 +1309,7 @@ describe('TypeScript Definitions', () => { }); it("should work with optional", () => { - const schema = { foo: { type: "number", optional: true } }; + const schema = { foo: { type: "number", optional: true } } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 3 })).toBe(true); @@ -1307,7 +1318,7 @@ describe('TypeScript Definitions', () => { }); it("should work with optional and default", () => { - const schema = { foo: { type: "number", optional: true, default: 5 } }; + const schema = { foo: { type: "number", optional: true, default: 5 } } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 3 })).toBe(true); @@ -1322,7 +1333,7 @@ describe('TypeScript Definitions', () => { }); it("should not accept null value even if optional", () => { - const schema = { foo: { type: "number", nullable: false, optional: true } }; + const schema = { foo: { type: "number", nullable: false, optional: true } } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 3 })).toBe(true); @@ -1332,7 +1343,7 @@ describe('TypeScript Definitions', () => { }); it("should not accept null as value", () => { - const schema = {foo: {type: "number", nullable: false}}; + const schema = {foo: {type: "number", nullable: false}} satisfies ValidationSchema; const check = v.compile(schema); expect(check({ foo: 3 })).toBe(true); @@ -1348,13 +1359,13 @@ describe("Test async mode", () => { const v = new Validator({ useNewCustomCheckerFunction: true }); // Async mode 1 - const custom1 = jest.fn(async value => { + const custom1 = vi.fn(async value => { await new Promise(resolve => setTimeout(resolve, 100)); return value.toUpperCase(); }); // Async mode 2 - const custom2 = jest.fn(async (value) => { + const custom2 = vi.fn(async (value) => { await new Promise(resolve => setTimeout(resolve, 100)); return value.trim(); }); @@ -1380,7 +1391,7 @@ describe("Test async mode", () => { name: { type: "string", custom: custom1 }, username: { type: "custom", custom: custom2 }, age: { type: "even" } - }; + } satisfies ValidationSchema; const check = v.compile(schema); it("should be async", () => { @@ -1432,10 +1443,10 @@ describe("Test context meta", () => { name: { type: "string", custom: (value, errors, schema, name, parent, context) => { expect(context.meta).toEqual({ a: "from-meta" }); - return context.meta.a; + return context.meta?.a; } }, - }; + } satisfies ValidationSchema; const check = v.compile(schema); it("should call custom async validators", () => { diff --git a/test/typescript/rules/array.spec.ts b/test/typescript/rules/array.spec.ts index a17e00f1..58a30395 100644 --- a/test/typescript/rules/array.spec.ts +++ b/test/typescript/rules/array.spec.ts @@ -9,12 +9,19 @@ describe('TypeScript Definitions', () => { const check = v.compile({ $$root: true, type: 'array' }); const message = 'The \'\' field must be an array.'; + // @ts-expect-error expect(check(0)).toEqual([{ type: 'array', actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: 'array', actual: 1, message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: 'array', actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: 'array', actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: 'array', actual: true, message }]); + // @ts-expect-error expect(check('')).toEqual([{ type: 'array', actual: '', message }]); + // @ts-expect-error expect(check('test')).toEqual([{ type: 'array', actual: 'test', message }]); expect(check([])).toEqual(true); @@ -74,12 +81,12 @@ describe('TypeScript Definitions', () => { const check = v.compile({ $$root: true, type: 'array', enum: ['male', 'female'] } as RuleArray); expect(check(['human'])). - toEqual( +toEqual( [{ type: 'arrayEnum', actual: 'human', expected: 'male, female', message: 'The \'human\' value in \'\' field does not match any of the \'male, female\' values.' }]); expect(check(['male'])).toEqual(true); expect(check(['male', 'female'])).toEqual(true); expect(check(['male', 'female', 'human'])). - toEqual( +toEqual( [{ type: 'arrayEnum', actual: 'human', expected: 'male, female', message: 'The \'human\' value in \'\' field does not match any of the \'male, female\' values.' }]); }); @@ -88,6 +95,7 @@ describe('TypeScript Definitions', () => { expect(check([])).toEqual(true); expect(check(['human'])).toEqual(true); + // @ts-expect-error expect(check(['male', 3, 'female', true])).toEqual([ { type: 'string', field: '[1]', actual: 3, message: 'The \'[1]\' field must be a string.' }, { type: 'string', field: '[3]', actual: true, message: 'The \'[3]\' field must be a string.' }, @@ -160,11 +168,13 @@ describe('TypeScript Definitions', () => { it ("should not convert into array if null or undefined", () => { // Null check const value = { data: null }; + // @ts-expect-error expect(check(value)).toEqual([{ type: "required", field: "data", actual: null, message: "The 'data' field is required." }]); expect(value.data).toEqual(null); // Undefined check const value2 = { data: undefined }; - expect(check(value2)).toEqual([{ type: "required", field: "data", actual: undefined, message: "The 'data' field is required." }]); + // @ts-expect-error + expect(check(value2)).toEqual([{ type: "required", field: "data", actual: undefined, message: "The 'data' field is required." }]); expect(value2.data).toEqual(undefined); }); diff --git a/test/typescript/rules/boolean.spec.ts b/test/typescript/rules/boolean.spec.ts index c4c83300..1bef1fd8 100644 --- a/test/typescript/rules/boolean.spec.ts +++ b/test/typescript/rules/boolean.spec.ts @@ -8,20 +8,27 @@ describe("TypeScript Definitions", () => { const check = v.compile({ $$root: true, type: "boolean" }); const message = "The '' field must be a boolean."; + // @ts-expect-error expect(check(0)).toEqual([{ type: "boolean", actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: "boolean", actual: 1, message }]); + // @ts-expect-error expect(check("")).toEqual([ { type: "boolean", actual: "", message }, ]); + // @ts-expect-error expect(check("true")).toEqual([ { type: "boolean", actual: "true", message }, ]); + // @ts-expect-error expect(check("false")).toEqual([ { type: "boolean", actual: "false", message }, ]); + // @ts-expect-error expect(check([])).toEqual([ { type: "boolean", actual: [], message }, ]); + // @ts-expect-error expect(check({})).toEqual([ { type: "boolean", actual: {}, message }, ]); @@ -40,6 +47,7 @@ describe("TypeScript Definitions", () => { expect(check(0)).toEqual(true); expect(check(1)).toEqual(true); + // @ts-expect-error expect(check("")).toEqual([ { type: "boolean", actual: "", message }, ]); @@ -47,9 +55,11 @@ describe("TypeScript Definitions", () => { expect(check("false")).toEqual(true); expect(check("on")).toEqual(true); expect(check("off")).toEqual(true); + // @ts-expect-error expect(check([])).toEqual([ { type: "boolean", actual: [], message }, ]); + // @ts-expect-error expect(check({})).toEqual([ { type: "boolean", actual: {}, message }, ]); @@ -64,7 +74,7 @@ describe("TypeScript Definitions", () => { }); let obj: { - status: number | boolean | "true" | "false" | "on" | "off"; + status: 0 | 1 | boolean | "true" | "false" | "on" | "off"; } = { status: 0 }; expect(check(obj)).toEqual(true); expect(obj).toEqual({ status: false }); diff --git a/test/typescript/rules/class.spec.ts b/test/typescript/rules/class.spec.ts index 2507db55..617be098 100644 --- a/test/typescript/rules/class.spec.ts +++ b/test/typescript/rules/class.spec.ts @@ -8,7 +8,9 @@ describe("Test rule: class", () => { const check = v.compile({ rawData: { type: "class", instanceOf: Buffer } }); const message = "The 'rawData' field must be an instance of the 'Buffer' class."; + // @ts-expect-error expect(check({ rawData: "1234" })).toEqual([{ type: "classInstanceOf", field: "rawData", actual: "1234", expected: "Buffer", message }]); + // @ts-expect-error expect(check({ rawData: [1, 2, 3] })).toEqual([{ type: "classInstanceOf", field: "rawData", actual: [1, 2, 3], expected: "Buffer", message }]); expect(check({ rawData: Buffer.from([1, 2, 3]) })).toEqual(true); expect(check({ rawData: Buffer.alloc(3) })).toEqual(true); diff --git a/test/typescript/rules/currency.spec.ts b/test/typescript/rules/currency.spec.ts new file mode 100644 index 00000000..e0222c67 --- /dev/null +++ b/test/typescript/rules/currency.spec.ts @@ -0,0 +1,186 @@ +import Validator from "../../.."; + +const v = new Validator(); + +describe("Test rule: currency", () => { + it("should have decimal optional, and correctly placed if present", () => { + const check = v.compile({ + $$root: true, + type: "currency", + currencySymbol: "$", + symbolOptional: true, + }); + expect(check("$12.2")).toEqual(true); + expect(check("$12,222.2")).toEqual(true); + expect(check("$12,222")).toEqual(true); + expect(check("$12,222.0")).toEqual(true); + expect(check("$1.22.00")).toEqual([ + { + actual: "$1.22.00", + field: undefined, + message: "The '' must be a valid currency format", + type: "currency", + }, + ]); + }); + + it("should check thousand separator placement is correct", () => { + const check = v.compile({ + $$root: true, + type: "currency", + currencySymbol: "$", + symbolOptional: true, + }); + expect(check("$12.2")).toEqual(true); + expect(check("$12,222.2")).toEqual(true); + expect(check("$122,222.2")).toEqual(true); + expect(check("$1234,222.2")).toEqual([ + { + actual: "$1234,222.2", + field: undefined, + message: "The '' must be a valid currency format", + type: "currency", + }, + ]); + expect(check("$1,2,222")).toEqual([ + { + actual: "$1,2,222", + field: undefined, + message: "The '' must be a valid currency format", + type: "currency", + }, + ]); + }); + + it("should not allow any currency symbol , if not supplied in schema", () => { + let check = v.compile({ $$root: true, type: "currency" }); + expect(check("12.2")).toEqual(true); + expect(check("$12.2")).toEqual([ + { + actual: "$12.2", + field: undefined, + message: "The '' must be a valid currency format", + type: "currency", + }, + ]); + }); + + it("should not allow any other currency symbol, other than supplied in schema", () => { + let check = v.compile({ + $$root: true, + type: "currency", + currencySymbol: "$", + symbolOptional: false, + }); + expect(check("$12.2")).toEqual(true); + expect(check("#12.2")).toEqual([ + { + actual: "#12.2", + field: undefined, + message: "The '' must be a valid currency format", + type: "currency", + }, + ]); + }); + + it("should keep currency symbol optional, if symbolOptional is true in schema", () => { + let check = v.compile({ + $$root: true, + type: "currency", + currencySymbol: "$", + symbolOptional: true, + }); + expect(check("$12.2")).toEqual(true); + expect(check("12.2")).toEqual(true); + expect(check("#12.2")).toEqual([ + { + actual: "#12.2", + field: undefined, + message: "The '' must be a valid currency format", + type: "currency", + }, + ]); + }); + + it("should allow negative currencies", () => { + let check = v.compile({ + $$root: true, + type: "currency", + currencySymbol: "$", + symbolOptional: true, + }); + expect(check("-12.2")).toEqual(true); + expect(check("$-12.2")).toEqual(true); + expect(check("-$12.2")).toEqual(true); + expect(check("-$-12.2")).toEqual([ + { + actual: "-$-12.2", + field: undefined, + message: "The '' must be a valid currency format", + type: "currency", + }, + ]); + }); + + it("should work correctly with supplied thousand and decimal separator", () => { + let check = v.compile({ + $$root: true, + type: "currency", + currencySymbol: "$", + symbolOptional: true, + thousandSeparator: ".", + decimalSeparator: ",", + }); + expect(check("$12,2")).toEqual(true); + expect(check("$12.222")).toEqual(true); + expect(check("$12.222,2")).toEqual(true); + expect(check("$12,222.2")).toEqual([ + { + actual: "$12,222.2", + field: undefined, + message: "The '' must be a valid currency format", + type: "currency", + }, + ]); + }); + it("should work correctly with supplied regex pattern", () => { + let check = v.compile({ + $$root: true, + type: "currency", + customRegex: /123/g, + }); + expect(check("123")).toEqual(true); + expect(check("134")).toEqual([ + { + actual: "134", + field: undefined, + message: "The '' must be a valid currency format", + type: "currency", + }, + ]); + }); + + it("should allow custom metas", async () => { + const schema = { + $$foo: { + foo: "bar", + }, + $$root: true, + type: "currency", + }; + const clonedSchema = { ...schema }; + const check = v.compile(schema); + + expect(schema).toStrictEqual(clonedSchema); + + expect(check("12.2")).toEqual(true); + expect(check("$12.2")).toEqual([ + { + actual: "$12.2", + field: undefined, + message: "The '' must be a valid currency format", + type: "currency", + }, + ]); + }); +}); diff --git a/test/typescript/rules/custom.spec.ts b/test/typescript/rules/custom.spec.ts index 2d39b1cc..225502ff 100644 --- a/test/typescript/rules/custom.spec.ts +++ b/test/typescript/rules/custom.spec.ts @@ -1,13 +1,11 @@ -import Validator, { RuleCustom, ValidationSchema, CheckerFunction } from '../../../'; - +import Validator, { ValidationSchema, CheckerFunction } from "../../../"; describe("Test rule: custom v1", () => { const v = new Validator(); - it("should call custom checker", () => { - const checker = jest.fn(() => true); - const schema = { $$root: true, type: "custom", a: 5, check: checker }; + const checker = vi.fn(() => true); + const schema = { $$root: true, type: "custom", a: 5, check: checker } satisfies ValidationSchema; const check = v.compile(schema); expect(check(10)).toEqual(true); @@ -16,8 +14,10 @@ describe("Test rule: custom v1", () => { }); it("should call custom checker", () => { - const checker = jest.fn((v) => v); - const schema = { weight: { type: "custom", a: 5, check: checker } }; + const checker = vi.fn((v) => v); + const schema = { + weight: { type: "custom", a: 5, check: checker } + } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ weight: 10 })).toEqual(true); @@ -28,35 +28,40 @@ describe("Test rule: custom v1", () => { it("should handle returned errors", () => { const fn: CheckerFunction = function (value, schema, field) { return [{ type: "myError", expected: 3, actual: 4 }]; - } + }; - const checker = jest.fn(fn); - const schema = { weight: { type: "custom", a: 5, check: checker, messages: { myError: "My error message. Expected: {expected}, actual: {actual}, field: {field}" } } }; + const checker = vi.fn(fn); + const schema = { weight: { type: "custom", a: 5, check: checker, messages: { myError: "My error message. Expected: {expected}, actual: {actual}, field: {field}" } } } satisfies ValidationSchema; const check = v.compile(schema); - expect(check({ weight: 10 })).toEqual([{ - type: "myError", - field: "weight", - actual: 4, - expected: 3, - message: "My error message. Expected: 3, actual: 4, field: weight" - }]); + expect(check({ weight: 10 })).toEqual([ + { + type: "myError", + field: "weight", + actual: 4, + expected: 3, + message: "My error message. Expected: 3, actual: 4, field: weight" + } + ]); expect(checker).toHaveBeenCalledTimes(1); expect(checker).toHaveBeenCalledWith(10, schema.weight, "weight", { weight: 10 }, expect.any(Object)); }); - }); -describe('TypeScript Definitions V2', () => { +describe("TypeScript Definitions V2", () => { const v = new Validator({ useNewCustomCheckerFunction: true }); - describe('Test rule: custom', () => { - - it('should call custom checker', () => { - const checker = jest.fn(() => true); - const schema: ValidationSchema = { $$root: true, type: 'custom', a: 5, check: checker } as RuleCustom; + describe("Test rule: custom", () => { + it("should call custom checker", () => { + const checker = vi.fn(() => true); + const schema = { + $$root: true, + type: 'custom', + a: 5, + check: checker + } satisfies ValidationSchema; const check = v.compile(schema); expect(check(10)).toEqual(true); @@ -64,9 +69,11 @@ describe('TypeScript Definitions V2', () => { expect(checker).toHaveBeenCalledWith(10, [], schema, 'null', null, expect.any(Object)); }); - it('should call custom checker', () => { - const checker = jest.fn((v) => v); - const schema = { weight: { type: 'custom', a: 5, check: checker } }; + it("should call custom checker", () => { + const checker = vi.fn((v) => v); + const schema = { + weight: { type: 'custom', a: 5, check: checker } + } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ weight: 10 })).toEqual(true); @@ -80,8 +87,8 @@ describe('TypeScript Definitions V2', () => { return value } - const checker = jest.fn(fn as any); - const schema = { weight: { type: 'custom', a: 5, check: checker, messages: { myError: 'My error message. Expected: {expected}, actual: {actual}, field: {field}' } } }; + const checker = vi.fn(fn as any); + const schema = { weight: { type: 'custom', a: 5, check: checker, messages: { myError: "My error message. Expected: {expected}, actual: {actual}, field: {field}" } } } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ weight: 10 })).toEqual([ diff --git a/test/typescript/rules/custom_messages.spec.ts b/test/typescript/rules/custom_messages.spec.ts index 3a2e693e..06175103 100644 --- a/test/typescript/rules/custom_messages.spec.ts +++ b/test/typescript/rules/custom_messages.spec.ts @@ -1,48 +1,55 @@ -import Validator, { RuleBoolean, RuleString, ValidationSchema } from '../../../'; +import Validator, { ValidationSchema } from '../../../'; const v = new Validator(); describe('TypeScript Definitions', () => { describe('Test custom messages', () => { - it('should give back not a string message', () => { - const message = 'That wasn\'t a string!'; - const s = { name: { type: 'string', messages: { string: message } } }; + const message = "That wasn't a string!"; + const s = { + name: { type: 'string', messages: { string: message } } + } satisfies ValidationSchema; expect(v.validate({ name: 123 }, s)).toEqual([{ type: 'string', actual: 123, field: 'name', message }]); }); it('should give back required message', () => { const message = 'Your name is required!'; - const s = { name: { type: 'string', messages: { required: message } } }; + const s = { + name: { type: 'string', messages: { required: message } } + } satisfies ValidationSchema; expect(v.validate({}, s)).toEqual([{ type: 'required', actual: undefined, field: 'name', message }]); - }); it('should do replacements in custom messages', () => { const message = 'Incorrect name length. Your field: {field} had {actual} chars when it should have no more than {expected}'; - const s = { name: { type: 'string', max: 2, messages: { stringMax: message } } }; - - expect(v.validate({ name: 'Long string' }, s)). - toEqual([ - { - type: 'stringMax', - expected: 2, - actual: 11, - field: 'name', - message: 'Incorrect name length. Your field: name had 11 chars when it should have no more than 2', - }]); + const s = { + name: { + type: 'string', + max: 2, + messages: { stringMax: message } + } + } satisfies ValidationSchema; + + expect(v.validate({ name: 'Long string' }, s)).toEqual([ + { + type: 'stringMax', + expected: 2, + actual: 11, + field: 'name', + message: 'Incorrect name length. Your field: name had 11 chars when it should have no more than 2', + }]); }); it('should do custom messages in arrays', () => { - const s: ValidationSchema = { + const s = { cache: [ - { type: 'string', messages: { string: 'Not a string' } } as RuleString, - { type: 'boolean', messages: { boolean: 'Not a boolean' } } as RuleBoolean, + { type: 'string', messages: { string: 'Not a string' } }, + { type: 'boolean', messages: { boolean: 'Not a boolean' } } ], - }; + } satisfies ValidationSchema; expect(v.validate({ cache: 123 }, s)).toEqual([ { type: 'string', field: 'cache', actual: 123, message: 'Not a string' }, @@ -66,15 +73,16 @@ describe('TypeScript Definitions', () => { }, }, }, - }; - - expect(v.validate({ - users: [ - { id: 'test', name: 'John', status: true }, - { id: 2, name: 123, status: true }, - { id: 3, name: 'Bill', status: false }, - ], - }, s)).toEqual([ + } satisfies ValidationSchema; + + expect( + v.validate({ + users: [ + { id: 'test', name: 'John', status: true }, + { id: 2, name: 123, status: true }, + { id: 3, name: 'Bill', status: false }, + ], + }, s)).toEqual([ { type: 'number', field: 'users[0].id', actual: 'test', message: 'numbers only please' }, { type: 'string', field: 'users[1].name', actual: 123, message: 'make sure it\'s a string' }, ]); @@ -94,19 +102,22 @@ describe('TypeScript Definitions', () => { }, }, }, - }; + } satisfies ValidationSchema; const check = v.compile(s); - expect(check({ - users: [ - { id: 'test', name: 'John', status: true }, - { id: 2, name: 123, status: true }, - { id: 3, name: 'Bill', status: false }, - ], - })).toEqual([ - { type: 'number', field: 'users[0].id', actual: 'test', message: 'numbers only please' }, - { type: 'string', field: 'users[1].name', actual: 123, message: 'make sure it\'s a string' }, + expect( + check({ + users: [ + // @ts-expect-error + { id: 'test', name: 'John', status: true }, + // @ts-expect-error + { id: 2, name: 123, status: true }, + { id: 3, name: 'Bill', status: false } + ], + })).toEqual([ + { type: 'number', field: 'users[0].id', actual: 'test', message: 'numbers only please' }, + { type: 'string', field: 'users[1].name', actual: 123, message: 'make sure it\'s a string' }, ]); }); diff --git a/test/typescript/rules/date.spec.ts b/test/typescript/rules/date.spec.ts index 45b5c7cc..02359f3e 100644 --- a/test/typescript/rules/date.spec.ts +++ b/test/typescript/rules/date.spec.ts @@ -9,15 +9,23 @@ describe('TypeScript Definitions', () => { const check = v.compile({ $$root: true, type: 'date' }); const message = 'The \'\' field must be a Date.'; + // @ts-expect-error expect(check(0)).toEqual([{ type: 'date', actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: 'date', actual: 1, message }]); + // @ts-expect-error expect(check('')).toEqual([{ type: 'date', actual: '', message }]); + // @ts-expect-error expect(check('true')).toEqual([{ type: 'date', actual: 'true', message }]); + // @ts-expect-error expect(check('false')).toEqual([{ type: 'date', actual: 'false', message }]); + // @ts-expect-error expect(check([])).toEqual([{ type: 'date', actual: [], message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: 'date', actual: {}, message }]); const now = Date.now(); + // @ts-expect-error expect(check(now)).toEqual([{ type: 'date', actual: now, message }]); expect(check(new Date())).toEqual(true); diff --git a/test/typescript/rules/email.spec.ts b/test/typescript/rules/email.spec.ts index 8d1f6a12..f6668985 100644 --- a/test/typescript/rules/email.spec.ts +++ b/test/typescript/rules/email.spec.ts @@ -1,31 +1,48 @@ -import Validator, { RuleEmail, RuleURL } from '../../../'; +import Validator, { RuleEmail } from "../../../"; const v = new Validator(); describe("Test rule: email", () => { it("should check empty values", () => { - const check = v.compile({ $$root: true, type: "email", empty: true } as RuleEmail); + const check = v.compile({ + $$root: true, + type: "email", + empty: true, + } satisfies RuleEmail); expect(check("john.doe@company.net")).toEqual(true); expect(check("")).toEqual(true); }); it("should check values", () => { - const check = v.compile({ $$root: true, type: "email" } as RuleEmail); + const rule = { + $$root: true, + type: "email", + } satisfies RuleEmail; + const check = v.compile(rule); const message = "The '' field must be a string."; + // @ts-expect-error expect(check(0)).toEqual([{ type: "string", actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: "string", actual: 1, message }]); expect(check("")).toEqual([{ type: "emailEmpty", actual: "", message: "The '' field must not be empty." }]); expect(check("true")).toEqual([{ type: "email", actual: "true", message: "The '' field must be a valid e-mail." }]); + // @ts-expect-error expect(check([])).toEqual([{ type: "string", actual: [], message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: "string", actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: "string", actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: "string", actual: true, message }]); }); it("should check values with quick pattern", () => { - const check = v.compile({ $$root: true, type: "email" } as RuleEmail); + const check = v.compile({ + $$root: true, + type: "email", + } satisfies RuleEmail); const message = "The '' field must be a valid e-mail."; expect(check("abcdefg")).toEqual([{ type: "email", actual: "abcdefg", message }]); @@ -42,7 +59,11 @@ describe("Test rule: email", () => { }); it("should check values", () => { - const check = v.compile({ $$root: true, type: "email", mode: "precise" } as RuleEmail); + const check = v.compile({ + $$root: true, + type: "email", + mode: "precise", + } satisfies RuleEmail); const message = "The '' field must be a valid e-mail."; expect(check("abcdefg")).toEqual([{ type: "email", actual: "abcdefg", message }]); @@ -57,7 +78,9 @@ describe("Test rule: email", () => { }); it("should not normalize", () => { - const check = v.compile({ email: { type: "email" } as RuleEmail }); + const check = v.compile({ + email: { type: "email" } satisfies RuleEmail, + }); const obj = { email: "John.Doe@Gmail.COM" }; expect(check(obj)).toEqual(true); @@ -67,7 +90,9 @@ describe("Test rule: email", () => { }); it("should normalize", () => { - const check = v.compile({ email: { type: "email", normalize: true } as RuleEmail }); + const check = v.compile({ + email: { type: "email", normalize: true } satisfies RuleEmail, + }); const obj = { email: " John.Doe@Gmail.COM " }; expect(check(obj)).toEqual(true); diff --git a/test/typescript/rules/enum.spec.ts b/test/typescript/rules/enum.spec.ts index c2237214..21f74385 100644 --- a/test/typescript/rules/enum.spec.ts +++ b/test/typescript/rules/enum.spec.ts @@ -1,4 +1,4 @@ -import Validator, { RuleEnum } from '../../../'; +import Validator from '../../../'; const v = new Validator(); @@ -6,10 +6,12 @@ describe('TypeScript Definitions', () => { describe('Test rule: enum', () => { it('check enum', () => { - const check = v.compile({ $$root: true, type: 'enum', values: ['male', 'female'] } as RuleEnum); + const check = v.compile({ $$root: true, type: 'enum', values: ['male', 'female'] }); + // @ts-expect-error expect(check('')). toEqual([{ type: 'enumValue', expected: 'male, female', actual: '', message: 'The \'\' field value \'male, female\' does not match any of the allowed values.' }]); + // @ts-expect-error expect(check('human')). toEqual([{ type: 'enumValue', expected: 'male, female', actual: 'human', message: 'The \'\' field value \'male, female\' does not match any of the allowed values.' }]); expect(check('male')).toEqual(true); @@ -17,8 +19,9 @@ describe('TypeScript Definitions', () => { }); it('check enum', () => { - const check = v.compile({ $$root: true, type: 'enum', values: [null, 1, 2, 'done', false] } as RuleEnum); + const check = v.compile({ $$root: true, type: 'enum', values: [null, 1, 2, 'done', false] } ); + // @ts-expect-error expect(check('male')). toEqual([ { diff --git a/test/typescript/rules/forbidden.spec.ts b/test/typescript/rules/forbidden.spec.ts index 7470d279..91ac9d37 100644 --- a/test/typescript/rules/forbidden.spec.ts +++ b/test/typescript/rules/forbidden.spec.ts @@ -10,30 +10,37 @@ describe('TypeScript Definitions', () => { const message = 'The \'\' field is forbidden.'; expect(check(null)).toEqual(true); expect(check(undefined)).toEqual(true); + // @ts-expect-error expect(check(0)).toEqual([{ type: 'forbidden', actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: 'forbidden', actual: 1, message }]); + // @ts-expect-error expect(check('')).toEqual([{ type: 'forbidden', actual: '', message }]); + // @ts-expect-error expect(check('null')).toEqual([{ type: 'forbidden', actual: 'null', message }]); + // @ts-expect-error expect(check([])).toEqual([{ type: 'forbidden', actual: [], message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: 'forbidden', actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: 'forbidden', actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: 'forbidden', actual: true, message }]); }); describe('Test sanitization', () => { - - it('should remove the field if \'remove: true\'', () => { + it("should remove the field if 'remove: true'", () => { let schema = { id: { type: 'number' }, name: { type: 'string' }, - token: { type: 'forbidden', remove: true }, - }; + token: { type: 'forbidden', remove: true } + } as const; let check = v.compile(schema); const obj = { id: 2, name: 'John', - }; + } as const; expect(check(obj)).toEqual(true); expect(obj).toEqual({ diff --git a/test/typescript/rules/function.spec.ts b/test/typescript/rules/function.spec.ts index 01a2c230..3cd9bb52 100644 --- a/test/typescript/rules/function.spec.ts +++ b/test/typescript/rules/function.spec.ts @@ -9,13 +9,21 @@ describe('TypeScript Definitions', () => { const check = v.compile({ $$root: true, type: 'function' }); const message = 'The \'\' field must be a function.'; + // @ts-expect-error expect(check(0)).toEqual([{ type: 'function', actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: 'function', actual: 1, message }]); + // @ts-expect-error expect(check('')).toEqual([{ type: 'function', actual: '', message }]); + // @ts-expect-error expect(check('true')).toEqual([{ type: 'function', actual: 'true', message }]); + // @ts-expect-error expect(check([])).toEqual([{ type: 'function', actual: [], message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: 'function', actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: 'function', actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: 'function', actual: true, message }]); expect(check(function () { })).toEqual(true); diff --git a/test/typescript/rules/luhn.spec.ts b/test/typescript/rules/luhn.spec.ts index 33f7e141..ac2072f4 100644 --- a/test/typescript/rules/luhn.spec.ts +++ b/test/typescript/rules/luhn.spec.ts @@ -9,11 +9,17 @@ describe('TypeScript Definitions', () => { const check = v.compile({ $$root: true, type: 'luhn' }); let message = 'The \'\' field must be a string.'; + // @ts-expect-error expect(check(0)).toEqual([{ type: 'string', actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: 'string', actual: 1, message }]); + // @ts-expect-error expect(check([])).toEqual([{ type: 'string', actual: [], message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: 'string', actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: 'string', actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: 'string', actual: true, message }]); message = 'The \'\' field must be a valid checksum luhn.'; diff --git a/test/typescript/rules/mac.spec.ts b/test/typescript/rules/mac.spec.ts index 1a0ae455..ebb6c11b 100644 --- a/test/typescript/rules/mac.spec.ts +++ b/test/typescript/rules/mac.spec.ts @@ -9,11 +9,17 @@ describe('TypeScript Definitions', () => { const check = v.compile({ $$root: true, type: 'mac' }); let message = 'The \'\' field must be a string.'; + // @ts-expect-error expect(check(0)).toEqual([{ type: 'string', actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: 'string', actual: 1, message }]); + // @ts-expect-error expect(check([])).toEqual([{ type: 'string', actual: [], message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: 'string', actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: 'string', actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: 'string', actual: true, message }]); message = 'The \'\' field must be a valid MAC address.'; diff --git a/test/typescript/rules/multi.spec.ts b/test/typescript/rules/multi.spec.ts new file mode 100644 index 00000000..09959e5f --- /dev/null +++ b/test/typescript/rules/multi.spec.ts @@ -0,0 +1,318 @@ +import Validator from "../../.."; + +describe("Test rule: multi", () => { + const v = new Validator({ + useNewCustomCheckerFunction: true, + }); + it("should call item's custom checker function", () => { + const fn = vi.fn((v) => v); + + const schema = { + $$root: true, + type: "multi", + rules: [ + { + type: "string", + custom: fn, + }, + { + type: "number", + custom: fn, + }, + ], + } as const; + + const check = v.compile(schema); + + check("s"); + expect(fn).toBeCalledTimes(1); + expect(fn).toBeCalledWith( + "s", + [], + schema.rules[0], + "$$root", + null, + expect.any(Object), + ); + }); + + // it("should value equals to other field", () => { + // // TODO: move from validator.spec.js + // }); + + describe("object strict test", function () { + it("should pass simple test", () => { + const v = new Validator({ + useNewCustomCheckerFunction: true, + }); + const check = v.compile({ + $$root: true, + type: "multi", + rules: ["string", "number"], + }); + expect(check(1)).toBe(true); + expect(check("1")).toBe(true); + // @ts-expect-error + expect(check({ a: 1 })).toEqual([ + { + actual: { a: 1 }, + field: undefined, + message: "The '' field must be a string.", + type: "string", + }, + { + actual: { a: 1 }, + field: undefined, + message: "The '' field must be a number.", + type: "number", + }, + ]); + }); + it("should pass object strict remove", () => { + const v = new Validator({ + useNewCustomCheckerFunction: true, + }); + + v.alias("targetA", { + type: "object", + strict: "remove", + props: { + a: "number", + }, + }); + + v.alias("targetB", { + type: "object", + strict: "remove", + props: { + b: "number", + }, + }); + + v.alias("targetC", { + type: "object", + props: { + c: "number", + }, + }); + + const check = v.compile({ + $$root: true, + type: "multi", + rules: ["targetA", "targetB", "targetC"], + }); + + expect(check({ a: 1 })).toBe(true); + + const testB = { b: 2, z: 3 }; + expect(check(testB)).toBe(true); + expect(testB).toEqual({ b: 2 }); + + const testC = { c: 3, d: 4 }; + expect(check(testC)).toBe(true); + expect(testC).toEqual({ c: 3, d: 4 }); + + expect(check({ d: 4 })).toEqual([ + { + actual: undefined, + field: "a", + message: "The 'a' field is required.", + type: "required", + }, + { + actual: undefined, + field: "b", + message: "The 'b' field is required.", + type: "required", + }, + { + actual: undefined, + field: "c", + message: "The 'c' field is required.", + type: "required", + }, + ]); + }); + + it("issue #297", () => { + const v = new Validator(); + const check = v.compile({ + $$strict: true, + age: "number", + name: "string", + surname: "string", + }); + + expect( + check({ + address: "London", + // @ts-expect-error + age: "22", + name: "John", + surname: "Doe", + }), + ).toEqual([ + { + type: "number", + message: "The 'age' field must be a number.", + field: "age", + actual: "22", + }, + { + type: "objectStrict", + message: + "The object '' contains forbidden keys: 'address'.", + expected: "age, name, surname", + actual: "address", + }, + ]); + }); + + it("issue #303 (nullable with shorthard format)", () => { + const v = new Validator(); + const check = v.compile({ + dateString: [ + { type: "string", nullable: true }, + { type: "boolean", nullable: true }, + ], + }); + + expect(check({ dateString: true })).toBe(true); + expect(check({ dateString: new Date().toISOString() })).toBe(true); + expect(check({ dateString: null })).toBe(true); + // @ts-expect-error + expect(check({})).toEqual([ + { + type: "required", + message: "The 'dateString' field is required.", + field: "dateString", + actual: undefined, + }, + ]); + }); + }); + + describe("should work with custom validator", () => { + const checkerFn = vi.fn(() => {}); + + const v = new Validator({ + useNewCustomCheckerFunction: true, + aliases: { + strOK: { + type: "string", + custom: (value, errors) => { + checkerFn(); + if (value !== "OK") { + errors.push({ type: "strOK" }); + return; + } + return value; + }, + }, + num99: { + type: "number", + custom: (value, errors) => { + checkerFn(); + if (value !== 99) { + errors.push({ type: "num99" }); + return; + } + return value; + }, + }, + }, + }); + + const schema = { + a: { + type: "multi", + rules: ["strOK", "num99"], + }, + } as const; + const check = v.compile(schema); + + it("test strOK", () => { + { + const o = { a: "OK" }; + expect(check(o)).toBe(true); + expect(o).toStrictEqual({ a: "OK" }); + expect(checkerFn).toBeCalledTimes(1); + } + { + const o = { a: "not-OK" }; + expect(check(o)).toStrictEqual([ + { field: "a", message: undefined, type: "strOK" }, + { + actual: "not-OK", + field: "a", + message: "The 'a' field must be a number.", + type: "number", + }, + { field: "a", message: undefined, type: "num99" }, + ]); + expect(o).toStrictEqual({ a: "not-OK" }); + expect(checkerFn).toBeCalledTimes(3); + } + }); + + it("test num99", () => { + { + const o = { a: 99 } as const; + expect(check(o)).toBe(true); + expect(o).toStrictEqual({ a: 99 }); + expect(checkerFn).toBeCalledTimes(5); + } + { + const o = { a: 1199 } as const; + expect(check(o)).toStrictEqual([ + { + actual: 1199, + field: "a", + message: "The 'a' field must be a string.", + type: "string", + }, + { field: "a", message: undefined, type: "strOK" }, + { field: "a", message: undefined, type: "num99" }, + ]); + expect(o).toStrictEqual({ a: 1199 }); + expect(checkerFn).toBeCalledTimes(7); + } + }); + }); + + it("should allow custom metas", async () => { + const fn = vi.fn((v) => v); + const schema = { + $$foo: { + foo: "bar", + }, + $$root: true, + type: "multi", + rules: [ + { + type: "string", + custom: fn, + }, + { + type: "number", + custom: fn, + }, + ], + } as const; + const clonedSchema = { ...schema }; + const check = v.compile(schema); + + expect(clonedSchema).toEqual(schema); + + check("s"); + expect(fn).toBeCalledTimes(1); + expect(fn).toBeCalledWith( + "s", + [], + schema.rules[0], + "$$root", + null, + expect.any(Object), + ); + }); +}); diff --git a/test/typescript/rules/number.spec.ts b/test/typescript/rules/number.spec.ts index 877ee4a2..77cde772 100644 --- a/test/typescript/rules/number.spec.ts +++ b/test/typescript/rules/number.spec.ts @@ -8,12 +8,19 @@ describe('TypeScript Definitions', () => { const check = v.compile({ $$root: true, type: 'number' }); const message = 'The \'\' field must be a number.'; + // @ts-expect-error expect(check('')).toEqual([{ type: 'number', actual: '', message }]); + // @ts-expect-error expect(check('test')).toEqual([{ type: 'number', actual: 'test', message }]); + // @ts-expect-error expect(check('1')).toEqual([{ type: 'number', actual: '1', message }]); + // @ts-expect-error expect(check([])).toEqual([{ type: 'number', actual: [], message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: 'number', actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: 'number', actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: 'number', actual: true, message }]); expect(check(NaN)).toEqual([{ type: 'number', actual: NaN, message }]); expect(check(Number.POSITIVE_INFINITY)).toEqual([{ type: 'number', actual: Number.POSITIVE_INFINITY, message }]); @@ -26,7 +33,7 @@ describe('TypeScript Definitions', () => { }); it('check min', () => { - const check = v.compile({ $$root: true, type: 'number', min: 5 } as RuleNumber); + const check = v.compile({ $$root: true, type: 'number', min: 5 } satisfies RuleNumber); const message = 'The \'\' field must be greater than or equal to 5.'; expect(check(3)).toEqual([{ type: 'numberMin', expected: 5, actual: 3, message }]); @@ -36,7 +43,7 @@ describe('TypeScript Definitions', () => { }); it('check max', () => { - const check = v.compile({ $$root: true, type: 'number', max: 5 } as RuleNumber); + const check = v.compile({ $$root: true, type: 'number', max: 5 } satisfies RuleNumber); const message = 'The \'\' field must be less than or equal to 5.'; expect(check(8)).toEqual([{ type: 'numberMax', expected: 5, actual: 8, message }]); @@ -47,7 +54,7 @@ describe('TypeScript Definitions', () => { }); it('check equal value', () => { - const check = v.compile({ $$root: true, type: 'number', equal: 123 } as RuleNumber); + const check = v.compile({ $$root: true, type: 'number', equal: 123 } satisfies RuleNumber); const message = 'The \'\' field must be equal to 123.'; expect(check(8)).toEqual([{ type: 'numberEqual', expected: 123, actual: 8, message }]); @@ -57,7 +64,7 @@ describe('TypeScript Definitions', () => { }); it('check not equal value', () => { - const check = v.compile({ $$root: true, type: 'number', notEqual: 123 } as RuleNumber); + const check = v.compile({ $$root: true, type: 'number', notEqual: 123 } satisfies RuleNumber); const message = 'The \'\' field can\'t be equal to 123.'; expect(check(8)).toEqual(true); @@ -67,7 +74,7 @@ describe('TypeScript Definitions', () => { }); it('check integer', () => { - const check = v.compile({ $$root: true, type: 'number', integer: true } as RuleNumber); + const check = v.compile({ $$root: true, type: 'number', integer: true } satisfies RuleNumber); const message = 'The \'\' field must be an integer.'; expect(check(8.5)).toEqual([{ type: 'numberInteger', actual: 8.5, message }]); @@ -79,7 +86,7 @@ describe('TypeScript Definitions', () => { }); it('check positive number', () => { - const check = v.compile({ $$root: true, type: 'number', positive: true } as RuleNumber); + const check = v.compile({ $$root: true, type: 'number', positive: true } satisfies RuleNumber); const message = 'The \'\' field must be a positive number.'; expect(check(-5.5)).toEqual([{ type: 'numberPositive', actual: -5.5, message }]); @@ -91,7 +98,7 @@ describe('TypeScript Definitions', () => { }); it('check negative number', () => { - const check = v.compile({ $$root: true, type: 'number', negative: true } as RuleNumber); + const check = v.compile({ $$root: true, type: 'number', negative: true } satisfies RuleNumber); const message = 'The \'\' field must be a negative number.'; expect(check(5.5)).toEqual([{ type: 'numberNegative', actual: 5.5, message }]); @@ -103,7 +110,7 @@ describe('TypeScript Definitions', () => { }); it('should convert & check values', () => { - const check = v.compile({ $$root: true, type: 'number', convert: true } as RuleNumber); + const check = v.compile({ $$root: true, type: 'number', convert: true } satisfies RuleNumber); const message = 'The \'\' field must be a number.'; expect(check({})).toEqual([{ type: 'number', actual: {}, message }]); @@ -120,7 +127,7 @@ describe('TypeScript Definitions', () => { }); it('should sanitize', () => { - const check = v.compile({ age: { type: 'number', convert: true } as RuleNumber }); + const check = v.compile({ age: { type: 'number', convert: true } satisfies RuleNumber }); let obj: ValidationSchema = { age: '' }; expect(check(obj)).toEqual(true); diff --git a/test/typescript/rules/object.spec.ts b/test/typescript/rules/object.spec.ts index 19524f65..a2bc0b27 100644 --- a/test/typescript/rules/object.spec.ts +++ b/test/typescript/rules/object.spec.ts @@ -9,18 +9,24 @@ describe('TypeScript Definitions', () => { const check = v.compile({ $$root: true, type: 'object' }); const message = 'The \'\' must be an Object.'; + // @ts-expect-error expect(check(0)).toEqual([{ type: 'object', actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: 'object', actual: 1, message }]); + // @ts-expect-error expect(check('')).toEqual([{ type: 'object', actual: '', message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: 'object', actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: 'object', actual: true, message }]); + // @ts-expect-error expect(check([])).toEqual([{ type: 'object', actual: [], message }]); expect(check({})).toEqual(true); expect(check({ a: 'John' })).toEqual(true); }); it('should check strict object', () => { - const check = v.compile({ $$root: true, type: 'object', strict: true, properties: {} } as RuleObject); + const check = v.compile({ $$root: true, type: 'object', strict: true, properties: {} } satisfies RuleObject); expect(check({})).toEqual(true); expect(check({ a: 'John' })).toEqual([{ type: 'objectStrict', actual: 'a', expected: '', message: 'The object \'\' contains forbidden keys: \'a\'.' }]); }); @@ -30,7 +36,8 @@ describe('TypeScript Definitions', () => { $$root: true, type: 'object', strict: true, props: { a: { type: 'string', trim: true }, }, - } as RuleObject); + } satisfies RuleObject); + // @ts-expect-error expect(check({ a: 'John', b: 'Doe' })).toEqual([{ type: 'objectStrict', actual: 'b', expected: 'a', message: 'The object \'\' contains forbidden keys: \'b\'.' }]); const o = { a: ' John' }; @@ -44,7 +51,8 @@ describe('TypeScript Definitions', () => { 'read-only': 'boolean', 'op.tional': { type: 'string', optional: true }, }, - } as RuleObject); + } satisfies RuleObject); + // @ts-expect-error expect(check({})).toEqual([{ type: 'required', field: 'read-only', actual: undefined, message: 'The \'read-only\' field is required.' }]); expect(check({ 'read-only': false })).toEqual(true); }); @@ -63,6 +71,7 @@ describe('TypeScript Definitions', () => { }, }, }); + // @ts-expect-error expect(check({ user: { firstName: 'John', address: { country: 'UK' } } })). toEqual([{ type: 'required', field: 'user.address.city', actual: undefined, message: 'The \'user.address.city\' field is required.' }]); }); @@ -78,7 +87,7 @@ describe('TypeScript Definitions', () => { city: 'string', }, }, - }; + } as const; let check = v.compile(schema); const obj = { @@ -89,7 +98,7 @@ describe('TypeScript Definitions', () => { street: 'Kossuth Lajos street', zip: 1234, }, - }; + } as const; expect(check(obj)).toEqual(true); expect(obj).toEqual({ diff --git a/test/typescript/rules/record.spec.ts b/test/typescript/rules/record.spec.ts new file mode 100644 index 00000000..ae0b562d --- /dev/null +++ b/test/typescript/rules/record.spec.ts @@ -0,0 +1,187 @@ +import Validator from "../../.."; + +const v = new Validator({ debug: false }); + +describe("Test rule: record", () => { + it("should check values", () => { + const check = v.compile({ $$root: true, type: "record" }); + const message = "The '' must be an Object."; + + // @ts-expect-error + expect(check(0)).toEqual([{ type: "record", actual: 0, message }]); + // @ts-expect-error + expect(check(1)).toEqual([{ type: "record", actual: 1, message }]); + // @ts-expect-error + expect(check("")).toEqual([{ type: "record", actual: "", message }]); + // @ts-expect-error + expect(check(false)).toEqual([ + { type: "record", actual: false, message }, + ]); + // @ts-expect-error + expect(check(true)).toEqual([ + { type: "record", actual: true, message }, + ]); + expect(check([])).toEqual([{ type: "record", actual: [], message }]); + expect(check({})).toEqual(true); + expect(check({ a: "John" })).toEqual(true); + }); + + it("should return key validation error when record has invalid key", () => { + const check = v.compile({ + $$root: true, + type: "record", + key: { type: "string", numeric: true }, + }); + + expect(check({ nonNumeric: 3 })).toEqual([ + { + type: "stringNumeric", + actual: "nonNumeric", + field: "nonNumeric", + message: "The 'nonNumeric' key must be a numeric string.", + }, + ]); + }); + + it("should return value validation error when record has invalid value", () => { + const check = v.compile({ + $$root: true, + type: "record", + value: { type: "number" }, + }); + + expect(check({ John: "Doe", Jane: 33 })).toEqual([ + { + type: "number", + actual: "Doe", + field: "John", + message: "The 'John' field must be a number.", + }, + ]); + }); + + it("should return value and key validation errors when record has invalid value and key", () => { + const check = v.compile({ + $$root: true, + type: "record", + key: { type: "string", alpha: true }, + value: { type: "string" }, + }); + + expect(check({ John: "Doe", 1: 2 })).toEqual([ + { + type: "stringAlpha", + actual: "1", + field: "1", + message: "The '1' key must be an alphabetic string.", + }, + { + type: "string", + actual: 2, + field: "1", + message: "The '1' field must be a string.", + }, + ]); + }); + + it("should pass validation when schema has only key rule", () => { + const check = v.compile({ + $$root: true, + type: "record", + key: { type: "string", alpha: true }, + }); + + expect(check({ John: "Doe", Jane: "Doe" })).toEqual(true); + }); + + describe("Test sanitization", () => { + it("should return sanitized record", async () => { + const check = v.compile({ + field: { + type: "record", + key: { type: "string", alpha: true, trim: true }, + value: { + type: "string", + alpha: true, + default: "Smith", + optional: true, + }, + }, + }); + + const value = { field: { John: "Doe", " Jane ": null } }; + expect(check(value)).toEqual(true); + expect(value).toEqual({ field: { John: "Doe", Jane: "Smith" } }); + }); + }); + + it.each([ + { rule: { type: "any" }, value: {} }, + { rule: { type: "array" }, value: [1, 2] }, + { rule: { type: "boolean" }, value: true }, + { rule: { type: "class", instanceOf: Number }, value: new Number() }, + { rule: { type: "currency", currencySymbol: "$" }, value: "$11.11" }, + { rule: { type: "date" }, value: new Date() }, + { rule: { type: "email" }, value: "user@example.com" }, + { rule: { type: "enum", values: ["John", "Jane"] }, value: "John" }, + { rule: { type: "forbidden" }, value: undefined }, + { rule: { type: "function" }, value: () => {} }, + { rule: { type: "luhn" }, value: "452373989901198" }, + { rule: { type: "mac" }, value: "01:C8:95:4B:65:FE" }, + { rule: { type: "multi", rules: ["number", "boolean"] }, value: 4 }, + { rule: { type: "number" }, value: 3 }, + { rule: { type: "object" }, value: {} }, + { rule: { type: "record" }, value: { test: "test" } }, + { rule: { type: "string" }, value: "example" }, + { rule: { type: "url" }, value: "https://example.com" }, + { + rule: { type: "uuid" }, + value: "10ba038e-48da-487b-96e8-8d3b99b6d18a", + }, + ])( + "should pass validation when schema has '$rule.type' rule as value", + ({ rule, value }) => { + const check = v.compile({ + $$root: true, + type: "record", + value: rule, + }); + + expect(check({ John: value })).toEqual(true); + }, + ); + + it("should allow custom metas", async () => { + const schema = { + $$foo: { + foo: "bar", + }, + $$root: true, + type: "record", + } as const; + const clonedSchema = { ...schema }; + const check = v.compile(schema); + + expect(clonedSchema).toEqual(schema); + + const message = "The '' must be an Object."; + + // @ts-expect-error + expect(check(0)).toEqual([{ type: "record", actual: 0, message }]); + // @ts-expect-error + expect(check(1)).toEqual([{ type: "record", actual: 1, message }]); + // @ts-expect-error + expect(check("")).toEqual([{ type: "record", actual: "", message }]); + // @ts-expect-error + expect(check(false)).toEqual([ + { type: "record", actual: false, message }, + ]); + // @ts-expect-error + expect(check(true)).toEqual([ + { type: "record", actual: true, message }, + ]); + expect(check([])).toEqual([{ type: "record", actual: [], message }]); + expect(check({})).toEqual(true); + expect(check({ a: "John" })).toEqual(true); + }); +}); diff --git a/test/typescript/rules/string.spec.ts b/test/typescript/rules/string.spec.ts index 870e2d2b..7e444f43 100644 --- a/test/typescript/rules/string.spec.ts +++ b/test/typescript/rules/string.spec.ts @@ -9,11 +9,17 @@ describe('TypeScript Definitions', () => { const check = v.compile({ $$root: true, type: 'string' }); const message = 'The \'\' field must be a string.'; + // @ts-expect-error expect(check(0)).toEqual([{ type: 'string', actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: 'string', actual: 1, message }]); + // @ts-expect-error expect(check([])).toEqual([{ type: 'string', actual: [], message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: 'string', actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: 'string', actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: 'string', actual: true, message }]); expect(check('')).toEqual(true); @@ -21,7 +27,7 @@ describe('TypeScript Definitions', () => { }); it('check empty values', () => { - const check = v.compile({ $$root: true, type: 'string', empty: false } as RuleString); + const check = v.compile({ $$root: true, type: 'string', empty: false } satisfies RuleString); expect(check('abc')).toEqual(true); expect(check('')).toEqual([{ type: 'stringEmpty', actual: '', message: 'The \'\' field must not be empty.' }]); @@ -54,28 +60,28 @@ describe('TypeScript Definitions', () => { }); it('check min length', () => { - const check = v.compile({ $$root: true, type: 'string', min: 5 } as RuleString); + const check = v.compile({ $$root: true, type: 'string', min: 5 } satisfies RuleString); expect(check('John')).toEqual([{ type: 'stringMin', expected: 5, actual: 4, message: 'The \'\' field length must be greater than or equal to 5 characters long.' }]); expect(check('Icebob')).toEqual(true); }); it('check max length', () => { - const check = v.compile({ $$root: true, type: 'string', max: 5 } as RuleString); + const check = v.compile({ $$root: true, type: 'string', max: 5 } satisfies RuleString); expect(check('John')).toEqual(true); expect(check('Icebob')).toEqual([{ type: 'stringMax', expected: 5, actual: 6, message: 'The \'\' field length must be less than or equal to 5 characters long.' }]); }); it('check fix length', () => { - const check = v.compile({ $$root: true, type: 'string', length: 6 } as RuleString); + const check = v.compile({ $$root: true, type: 'string', length: 6 } satisfies RuleString); expect(check('John')).toEqual([{ type: 'stringLength', expected: 6, actual: 4, message: 'The \'\' field length must be 6 characters long.' }]); expect(check('Icebob')).toEqual(true); }); it('check pattern', () => { - const check = v.compile({ $$root: true, type: 'string', pattern: /^[A-Z]+$/ } as RuleString); + const check = v.compile({ $$root: true, type: 'string', pattern: /^[A-Z]+$/ } satisfies RuleString); expect(check('John')).toEqual([{ type: 'stringPattern', expected: '/^[A-Z]+$/', actual: 'John', message: 'The \'\' field fails to match the required pattern.' }]); expect(check('JOHN')).toEqual(true); @@ -89,7 +95,7 @@ describe('TypeScript Definitions', () => { }); it('check pattern with a quote', () => { - const check = v.compile({ $$root: true, type: 'string', pattern: /^[a-z0-9 .\-'?!":;\\/,_]+$/i } as RuleString); + const check = v.compile({ $$root: true, type: 'string', pattern: /^[a-z0-9 .\-'?!":;\\/,_]+$/i } satisfies RuleString); expect(check('John^')).toEqual([{ field: undefined, type: 'stringPattern', expected: '/^[a-z0-9 .\-\'?!":;\\/,_]+$/i', actual: 'John^', message: 'The \'\' field fails to match the required pattern.' }]); expect(check('JOHN')).toEqual(true); @@ -109,7 +115,7 @@ describe('TypeScript Definitions', () => { }); it('check enum', () => { - const check = v.compile({ $$root: true, type: 'string', enum: ['male', 'female'] } as RuleString); + const check = v.compile({ $$root: true, type: 'string', enum: ['male', 'female'] } satisfies RuleString); const message = 'The \'\' field does not match any of the allowed values.'; expect(check('')).toEqual([{ type: 'stringEnum', expected: 'male, female', actual: '', message }]); @@ -186,7 +192,7 @@ describe('TypeScript Definitions', () => { }); it("check singleLine string", () => { - const schema: RuleString = { $$root: true, type: "string", singleLine: true } + const schema = { $$root: true, type: "string", singleLine: true } satisfies RuleString; const check = v.compile(schema); const message = "The '' field must be a single line string."; diff --git a/test/typescript/rules/tuple.spec.ts b/test/typescript/rules/tuple.spec.ts index 05f0c298..1eb3eb95 100644 --- a/test/typescript/rules/tuple.spec.ts +++ b/test/typescript/rules/tuple.spec.ts @@ -1,4 +1,8 @@ -import Validator, { RuleTuple, ValidationError } from '../../../'; +import Validator, { + RuleTuple, + ValidationError, + ValidationSchema, +} from "../../../"; const v = new Validator({ useNewCustomCheckerFunction: true, @@ -43,21 +47,28 @@ describe("TypeScript Definitions", () => { const check = v.compile({ $$root: true, type: "tuple" - } as RuleTuple); + } satisfies RuleTuple); const message = "The '' field must be an array."; + // @ts-expect-error expect(check(0)).toEqual([{ type: "tuple", actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: "tuple", actual: 1, message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: "tuple", actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([ - { type: "tuple", actual: false, message } + { type: "tuple", actual: false, message }, ]); + // @ts-expect-error expect(check(true)).toEqual([ - { type: "tuple", actual: true, message } + { type: "tuple", actual: true, message }, ]); + // @ts-expect-error expect(check("")).toEqual([{ type: "tuple", actual: "", message }]); + // @ts-expect-error expect(check("test")).toEqual([ - { type: "tuple", actual: "test", message } + { type: "tuple", actual: "test", message }, ]); expect(check([])).toEqual(true); @@ -68,7 +79,7 @@ describe("TypeScript Definitions", () => { $$root: true, type: "tuple", empty: false, - } as RuleTuple); + } satisfies RuleTuple); const message = "The '' field must not be an empty array."; expect(check([1])).toEqual(true); @@ -80,8 +91,8 @@ describe("TypeScript Definitions", () => { it("check length (w/o defined items)", () => { const check = v.compile({ $$root: true, - type: "tuple" - } as RuleTuple); + type: "tuple", + } satisfies RuleTuple); expect(check([1])).toEqual(true); expect(check([1, 2, 3])).toEqual(true); @@ -92,8 +103,8 @@ describe("TypeScript Definitions", () => { const check = v.compile({ $$root: true, type: "tuple", - items: ["boolean", "string"] - } as RuleTuple); + items: ["boolean", "string"], + } satisfies RuleTuple); const message = "The '' field must contain 2 items."; expect(check([1])).toEqual([ @@ -119,7 +130,7 @@ describe("TypeScript Definitions", () => { const check = v.compile({ $$root: true, type: "tuple" - } as RuleTuple); + } satisfies RuleTuple); expect(check([1])).toEqual(true); expect(check([1, 2, 3])).toEqual(true); @@ -131,7 +142,7 @@ describe("TypeScript Definitions", () => { $$root: true, type: "tuple", items: ["string", "number"] - } as RuleTuple); + } satisfies RuleTuple); expect(check([1, "human"])).toEqual([ { @@ -145,16 +156,16 @@ describe("TypeScript Definitions", () => { message: "The '[1]' field must be a number.", field: "[1]", actual: "human" - } + }, ]); expect(check(["male", 3])).toEqual(true); }); it("should call custom checker", () => { - const customFn = jest.fn(v => v); + const customFn = vi.fn((v) => v); const schema = { - pair: { type: "tuple", custom: customFn } as RuleTuple + pair: { type: "tuple", custom: customFn } satisfies RuleTuple, }; const check = v.compile(schema); @@ -171,8 +182,8 @@ describe("TypeScript Definitions", () => { }); it("should call custom checker for items", () => { - const customFn = jest.fn(v => v); - const customFnItems = jest.fn(v => v); + const customFn = vi.fn((v) => v); + const customFnItems = vi.fn((v) => v); const schema = { pair: { type: "tuple", @@ -187,8 +198,8 @@ describe("TypeScript Definitions", () => { custom: customFnItems } ] - } as RuleTuple - }; + } + } satisfies ValidationSchema; const check = v.compile(schema); expect(check({ pair: ["Pizza", true] })).toEqual(true); @@ -226,8 +237,8 @@ describe("TypeScript Definitions", () => { describe("Test sanitization", () => { it("should untouch the checked obj", () => { let schema = { - roles: { type: "tuple" } as RuleTuple - }; + roles: { type: "tuple" } satisfies RuleTuple, + } as const; let check = v.compile(schema); const obj = { @@ -251,9 +262,9 @@ describe("TypeScript Definitions", () => { type: "tuple", items: [ { type: "number", custom: customFn }, - { type: "number", custom: customFn } + { type: "number", custom: customFn }, ] - } as RuleTuple + } satisfies RuleTuple }); const o = { diff --git a/test/typescript/rules/url.spec.ts b/test/typescript/rules/url.spec.ts index 2d71e69b..d9b0a71c 100644 --- a/test/typescript/rules/url.spec.ts +++ b/test/typescript/rules/url.spec.ts @@ -5,21 +5,27 @@ const v = new Validator(); describe('TypeScript Definitions', () => { describe('Test rule: url', () => { it("should check empty values", () => { - const check = v.compile({ $$root: true, type: "url", empty: true } as RuleURL); + const check = v.compile({ $$root: true, type: "url", empty: true } satisfies RuleURL); expect(check("https://google.com")).toEqual(true); expect(check("")).toEqual(true); }); it('should check values', () => { - const check = v.compile({ $$root: true, type: 'url' } as RuleURL); + const check = v.compile({ $$root: true, type: 'url' } satisfies RuleURL); let message = 'The \'\' field must be a string.'; + // @ts-expect-error expect(check(0)).toEqual([{ type: 'string', actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: 'string', actual: 1, message }]); + // @ts-expect-error expect(check([])).toEqual([{ type: 'string', actual: [], message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: 'string', actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: 'string', actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: 'string', actual: true, message }]); message = 'The \'\' field must be a valid URL.'; diff --git a/test/typescript/rules/uuid.spec.ts b/test/typescript/rules/uuid.spec.ts index c1c9a5e2..d523ca98 100644 --- a/test/typescript/rules/uuid.spec.ts +++ b/test/typescript/rules/uuid.spec.ts @@ -8,11 +8,17 @@ describe('TypeScript Definitions', () => { const check = v.compile({ $$root: true, type: 'uuid' }); let message = 'The \'\' field must be a string.'; + // @ts-expect-error expect(check(0)).toEqual([{ type: 'string', actual: 0, message }]); + // @ts-expect-error expect(check(1)).toEqual([{ type: 'string', actual: 1, message }]); + // @ts-expect-error expect(check([])).toEqual([{ type: 'string', actual: [], message }]); + // @ts-expect-error expect(check({})).toEqual([{ type: 'string', actual: {}, message }]); + // @ts-expect-error expect(check(false)).toEqual([{ type: 'string', actual: false, message }]); + // @ts-expect-error expect(check(true)).toEqual([{ type: 'string', actual: true, message }]); message = 'The \'\' field must be a valid UUID.'; @@ -30,13 +36,13 @@ describe('TypeScript Definitions', () => { expect(check('00000000-0000-7000-0000-000000000000')).toEqual([{ type: 'uuid', actual: '00000000-0000-7000-0000-000000000000', message }]); expect(check('fdda765f-fc57-5604-c269-52a7df8164ec')).toEqual([{ type: 'uuid', actual: 'fdda765f-fc57-5604-c269-52a7df8164ec', message }]); - const check0 = v.compile({ $$root: true, type: "uuid", version: 0 } as RuleUUID); - const check1 = v.compile({ $$root: true, type: 'uuid', version: 1 } as RuleUUID); - const check2 = v.compile({ $$root: true, type: 'uuid', version: 2 } as RuleUUID); - const check3 = v.compile({ $$root: true, type: 'uuid', version: 3 } as RuleUUID); - const check4 = v.compile({ $$root: true, type: 'uuid', version: 4 } as RuleUUID); - const check5 = v.compile({ $$root: true, type: 'uuid', version: 5 } as RuleUUID); - const check7 = v.compile({ $$root: true, type: 'uuid', version: 7 } as RuleUUID); + const check0 = v.compile({ $$root: true, type: "uuid", version: 0 } satisfies RuleUUID); + const check1 = v.compile({ $$root: true, type: 'uuid', version: 1 } satisfies RuleUUID); + const check2 = v.compile({ $$root: true, type: 'uuid', version: 2 } satisfies RuleUUID); + const check3 = v.compile({ $$root: true, type: 'uuid', version: 3 } satisfies RuleUUID); + const check4 = v.compile({ $$root: true, type: 'uuid', version: 4 } satisfies RuleUUID); + const check5 = v.compile({ $$root: true, type: 'uuid', version: 5 } satisfies RuleUUID); + const check7 = v.compile({ $$root: true, type: 'uuid', version: 7 } satisfies RuleUUID); message = 'The \'\' field must be a valid UUID version provided.'; expect(check0("00000000-0000-1000-0000-000000000000")).toEqual([{ "actual": 1, "expected": 0, "type": "uuidVersion", message }]); @@ -50,15 +56,15 @@ describe('TypeScript Definitions', () => { }); it('check valid version', () => { - const check0 = v.compile({ $$root: true, type: "uuid", version: 0 } as RuleUUID); - const check1 = v.compile({ $$root: true, type: 'uuid', version: 1 } as RuleUUID); - const check2 = v.compile({ $$root: true, type: 'uuid', version: 2 } as RuleUUID); - const check3 = v.compile({ $$root: true, type: 'uuid', version: 3 } as RuleUUID); - const check4 = v.compile({ $$root: true, type: 'uuid', version: 4 } as RuleUUID); - const check5 = v.compile({ $$root: true, type: 'uuid', version: 5 } as RuleUUID); - const check6 = v.compile({ $$root: true, type: 'uuid', version: 6 } as RuleUUID); - const check7 = v.compile({ $$root: true, type: 'uuid', version: 7 } as RuleUUID); - const check8 = v.compile({ $$root: true, type: 'uuid', version: 8 } as RuleUUID); + const check0 = v.compile({ $$root: true, type: "uuid", version: 0 } satisfies RuleUUID); + const check1 = v.compile({ $$root: true, type: 'uuid', version: 1 } satisfies RuleUUID); + const check2 = v.compile({ $$root: true, type: 'uuid', version: 2 } satisfies RuleUUID); + const check3 = v.compile({ $$root: true, type: 'uuid', version: 3 } satisfies RuleUUID); + const check4 = v.compile({ $$root: true, type: 'uuid', version: 4 } satisfies RuleUUID); + const check5 = v.compile({ $$root: true, type: 'uuid', version: 5 } satisfies RuleUUID); + const check6 = v.compile({ $$root: true, type: 'uuid', version: 6 } satisfies RuleUUID); + const check7 = v.compile({ $$root: true, type: 'uuid', version: 7 } satisfies RuleUUID); + const check8 = v.compile({ $$root: true, type: 'uuid', version: 8 } satisfies RuleUUID); expect(check0("00000000-0000-0000-0000-000000000000")).toEqual(true); expect(check1('45745c60-7b1a-11e8-9c9c-2d42b21b1a3e')).toEqual(true); @@ -72,14 +78,14 @@ describe('TypeScript Definitions', () => { }); it("should not be case insensitive", () => { - const check1 = v.compile({ $$root: true, type: "uuid", version: 1 } as RuleUUID); - const check2 = v.compile({ $$root: true, type: "uuid", version: 2 } as RuleUUID); - const check3 = v.compile({ $$root: true, type: "uuid", version: 3 } as RuleUUID); - const check4 = v.compile({ $$root: true, type: "uuid", version: 4 } as RuleUUID); - const check5 = v.compile({ $$root: true, type: "uuid", version: 5 } as RuleUUID); - const check6 = v.compile({ $$root: true, type: "uuid", version: 6 } as RuleUUID); - const check7 = v.compile({ $$root: true, type: 'uuid', version: 7 } as RuleUUID); - const check8 = v.compile({ $$root: true, type: 'uuid', version: 8 } as RuleUUID); + const check1 = v.compile({ $$root: true, type: "uuid", version: 1 } satisfies RuleUUID); + const check2 = v.compile({ $$root: true, type: "uuid", version: 2 } satisfies RuleUUID); + const check3 = v.compile({ $$root: true, type: "uuid", version: 3 } satisfies RuleUUID); + const check4 = v.compile({ $$root: true, type: "uuid", version: 4 } satisfies RuleUUID); + const check5 = v.compile({ $$root: true, type: "uuid", version: 5 } satisfies RuleUUID); + const check6 = v.compile({ $$root: true, type: "uuid", version: 6 } satisfies RuleUUID); + const check7 = v.compile({ $$root: true, type: 'uuid', version: 7 } satisfies RuleUUID); + const check8 = v.compile({ $$root: true, type: 'uuid', version: 8 } satisfies RuleUUID); expect(check1("45745c60-7b1a-11e8-9c9c-2d42b21b1a3e")).toEqual(true); expect(check2("9a7b330a-a736-21e5-af7f-feaf819cdc9f")).toEqual(true); diff --git a/test/typescript/tsconfig.json b/test/typescript/tsconfig.json index 49fcee10..dae66fcf 100644 --- a/test/typescript/tsconfig.json +++ b/test/typescript/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { "target": "es6", - "lib": [ "es2015" ], + "lib": ["ESNext"], "sourceMap": false, - "module": "commonjs", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "isolatedModules": false, "experimentalDecorators": true, "emitDecoratorMetadata": true, @@ -12,13 +12,12 @@ "noImplicitAny": false, "removeComments": true, "noLib": false, - "strict": true, - "strictFunctionTypes": false, + "strict": true, + "strictFunctionTypes": false, "preserveConstEnums": true, - "suppressImplicitAnyIndexErrors": true, - "esModuleInterop": true, - "baseUrl": ".", - "allowJs": true - }, - "include": ["test", "lib", "index.d.ts", "index.js"] + "esModuleInterop": true, + "paths": { "*": ["./*"] }, + "allowJs": true, + "types": ["vitest/globals"] + } } diff --git a/test/typescript/validator.spec.ts b/test/typescript/validator.spec.ts index cbf1c369..ab0784f0 100644 --- a/test/typescript/validator.spec.ts +++ b/test/typescript/validator.spec.ts @@ -54,8 +54,8 @@ describe('TypeScript Definitions', () => { describe('Test validate', () => { const v = new Validator(); - const compiledFn = jest.fn(() => true); - v.compile = jest.fn(() => compiledFn) as any; + const compiledFn = vi.fn(() => true); + v.compile = vi.fn(() => compiledFn) as any; const schema = { name: { type: 'string' }, @@ -87,7 +87,7 @@ describe('TypeScript Definitions', () => { }, }); - const validFn = jest.fn(function (this: Validator, { messages }) { + const validFn = vi.fn(function (this: Validator, { messages }) { return { source: ` if (value % 2 != 0)