From e94aef702dc1481ec6d63cf1c43283d22401500c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zg=C3=BCr=20Adem=20I=C5=9EIKLI?= Date: Mon, 13 Jan 2025 17:27:33 +0100 Subject: [PATCH] Fixed #54 - Array of Object validation --- index.ts | 1 + src/Interface.ts | 11 +++ src/helpers/newValidate.ts | 63 +++++++++++++ src/helpers/validate.ts | 175 +++++++++++++++++++++++++------------ tests/index.test.ts | 2 +- tests/nested.test.ts | 137 +++++++++++++++++++++++++++++ 6 files changed, 331 insertions(+), 58 deletions(-) create mode 100644 src/helpers/newValidate.ts create mode 100644 tests/nested.test.ts diff --git a/index.ts b/index.ts index 290680c..1b6c708 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,7 @@ export * from "./src/Options"; export * from "./src/ruleManager"; export * from "./src/helpers/validate"; +export * from "./src/helpers/newValidate"; export * from "./src/Locale"; export * from "./src/rules"; export * from "./src/converters"; diff --git a/src/Interface.ts b/src/Interface.ts index 744b1bb..2b3e73b 100644 --- a/src/Interface.ts +++ b/src/Interface.ts @@ -44,3 +44,14 @@ export interface ILocale { key: LanguageType; values: Translation; } + +export interface ITraversePair { + path: string; + value: any; +} + +export interface ITraverseItem { + path: string; + rules: string | string[]; + resolved: Array; +} diff --git a/src/helpers/newValidate.ts b/src/helpers/newValidate.ts new file mode 100644 index 0000000..505847f --- /dev/null +++ b/src/helpers/newValidate.ts @@ -0,0 +1,63 @@ +import { ITraverseItem } from "../Interface"; + +export const toTraverseArray = async ( + data: any, + definition: Record +) => { + function resolvePath(data: any, path: string) { + const parts = path.split("."); + const result: Array<{ path: string; value: any }> = []; + + function traverse( + current: any, + index = 0, + resolvedPath: Array = [] + ) { + if (index >= parts.length) { + result.push({ path: resolvedPath.join("."), value: current }); + return; + } + + const part = parts[index]; + + if (part === "*") { + if (Array.isArray(current)) { + current.forEach((item, i) => { + traverse(item, index + 1, [...resolvedPath, i]); + }); + } else if (current && typeof current === "object") { + Object.keys(current).forEach((key) => { + traverse(current[key], index + 1, [...resolvedPath, key]); + }); + } else { + result.push({ + path: [...resolvedPath, "*"].join("."), + value: current, + }); + } + } else { + if (current && typeof current === "object" && part in current) { + traverse(current[part], index + 1, [...resolvedPath, part]); + } else { + result.push({ + path: [...resolvedPath, part].join("."), + value: undefined, + }); + } + } + } + + traverse(data); + return result; + } + + const checks: ITraverseItem[] = []; + + // Example usage + Object.entries(definition).forEach(([path, rules]) => { + const resolved = resolvePath(data, path); + checks.push({ path, rules, resolved }); + }); + + return checks; +}; diff --git a/src/helpers/validate.ts b/src/helpers/validate.ts index e292758..cb3cb60 100644 --- a/src/helpers/validate.ts +++ b/src/helpers/validate.ts @@ -1,8 +1,12 @@ -import { IContext, IValidationOptions, IValidationResult } from "../Interface"; +import { + IContext, + ITraverseItem, + IValidationOptions, + IValidationResult, +} from "../Interface"; import { getMessage } from "../Locale"; import { Definition, ValidationResult } from "../Types"; import { toRuleDefinition } from "../Factory"; -import { getValueViaPath } from "./getValueViaPath"; import { getOptions } from "../Options"; export const validate = async ( @@ -29,6 +33,65 @@ export const validate = async ( }; }; +const toTraverseArray = (data: any, definition: Definition) => { + function resolvePath(data: any, path: string) { + const parts = path.split("."); + const result: Array<{ path: string; value: any }> = []; + + function traverse( + current: any, + index = 0, + resolvedPath: Array = [] + ) { + if (index >= parts.length) { + result.push({ path: resolvedPath.join("."), value: current }); + return; + } + + const part = parts[index]; + + if (part === "*") { + if (Array.isArray(current)) { + current.forEach((item, i) => { + traverse(item, index + 1, [...resolvedPath, i]); + }); + } else if (current && typeof current === "object") { + Object.keys(current).forEach((key) => { + traverse(current[key], index + 1, [...resolvedPath, key]); + }); + } else { + result.push({ + path: [...resolvedPath, "*"].join("."), + value: current, + }); + } + } else { + if (current && typeof current === "object" && part in current) { + traverse(current[part], index + 1, [...resolvedPath, part]); + } else { + result.push({ + path: [...resolvedPath, part].join("."), + value: undefined, + }); + } + } + } + + traverse(data); + return result; + } + + const checks: ITraverseItem[] = []; + + // Example usage + Object.entries(definition).forEach(([path, rules]) => { + const resolved = resolvePath(data, path); + checks.push({ path, rules, resolved }); + }); + + return checks; +}; + const getResults = async ( data: any, definition: Definition, @@ -38,69 +101,63 @@ const getResults = async ( const fields: Record = {}; const results: ValidationResult = {}; - // Checking all validations - for (const field in definition) { - fields[field] = true; - // Parsing the rules - const params = definition[field]; - let ruleGroup: string = ""; - if (Array.isArray(params)) { - ruleGroup = params.join("|"); - } else { - ruleGroup = params; - } + const traverse = toTraverseArray(data, definition); - const rules = toRuleNameArray(ruleGroup).map(toRuleDefinition); + for (const item of traverse) { + const { path, rules, resolved } = item; + fields[path] = true; - // Getting the value by the path - const value = getValueViaPath(data, field); + const rulesAsString = Array.isArray(rules) ? rules.join("|") : rules; + + const ruleDefinitions = + toRuleNameArray(rulesAsString).map(toRuleDefinition); const context: IContext = { data, - field, - definition: ruleGroup, + field: path, + definition: rulesAsString, }; - // Checking all rules one by one - for (const rule of rules) { - // If the value is empty but the rule is not required, we don't execute - // the rules - if (rule.name !== "required" && (value === null || value === undefined)) { - continue; - } - - // Calling the rule function with the validation parameters - const isRuleValid = await rule.callback( - value, - ...[...rule.params, context] - ); - - // Is the value valid? - if (isRuleValid === false) { - if (!results[field]) { - results[field] = []; + for (const check of resolved) { + // Checking all rules one by one + for (const rule of ruleDefinitions) { + // If the value is empty but the rule is not required, we don't execute + // the rules + if ( + rule.name !== "required" && + (check.value === null || check.value === undefined) + ) { + continue; } - - isValid = false; - fields[field] = false; - - // Setting the rule and the error message - results[field].push({ - rule: rule.name, - message: getMessage( - rule.name, - rule.params, - options.language, - options.translations || {} - ), - }); - - if (options.stopOnFail) { - return { - isValid: false, - fields, - results, - }; + // Calling the rule function with the validation parameters + const isRuleValid = await rule.callback( + check.value, + ...[...rule.params, context] + ); + // Is the value valid? + if (isRuleValid === false) { + if (!results[check.path]) { + results[check.path] = []; + } + isValid = false; + fields[path] = false; + // Setting the rule and the error message + results[check.path].push({ + rule: rule.name, + message: getMessage( + rule.name, + rule.params, + options.language, + options.translations || {} + ), + }); + if (options.stopOnFail) { + return { + isValid: false, + fields, + results, + }; + } } } } @@ -114,5 +171,9 @@ const getResults = async ( }; const toRuleNameArray = (rules: string): string[] => { + if (Array.isArray(rules)) { + return rules; + } + return rules.split("|"); }; diff --git a/tests/index.test.ts b/tests/index.test.ts index 967ff04..1dfd472 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -19,7 +19,7 @@ const EXISTS_RULE_TRANSLATIONS = { describe("validate() function ", () => { beforeAll(async () => { - await setLocales(en as ILocale); + setLocales(en as ILocale); }); test("should be able to validate the general structure", async () => { diff --git a/tests/nested.test.ts b/tests/nested.test.ts new file mode 100644 index 0000000..83945cf --- /dev/null +++ b/tests/nested.test.ts @@ -0,0 +1,137 @@ +import { beforeAll, describe, test, expect } from "vitest"; +import { validate, setLocales, en, ILocale } from "../index"; + +describe("validate()", () => { + beforeAll(async () => { + setLocales(en as ILocale); + }); + + // test("should validate nested objects", async () => { + // const result = await validate( + // { + // id: 1, + // token: "123", + // user: { + // id: 1, + // email: "email", + // }, + // }, + // { + // id: "required|numeric", + // token: "required|min:20", + // "user.email": "required|email", + // } + // ); + // expect(result.isValid).toBe(false); + // expect(result.fields.id).toBe(true); + // expect(result.fields.token).toBe(false); + // expect(result.fields["user.email"]).toBe(false); + + // expect(result.errors["user.email"][0].message).toBe( + // "The field must be an email." + // ); + // }); + + // test("should validate arrays", async () => { + // const result = await validate( + // { + // users: [ + // { + // email: "correct@mail.com", + // }, + // { email: "email" }, + // ], + // }, + // { + // "users.*.email": "required|email", + // } + // ); + // expect(result.isValid).toBe(false); + // expect(result.fields["users.*.email"]).toBe(false); + // expect(result.errors["users.1.email"][0].message).toBe( + // "The field must be an email." + // ); + // }); + + test("should validate nested arrays", async () => { + const result = await validate( + { + users: [ + { + addresses: [ + { + city: "New York", + }, + { + street: "Wall Street", + }, + ], + }, + { + addresses: [ + { + city: "New York", + }, + { + city: "Los Angeles", + }, + ], + }, + ], + }, + { + "users.*.addresses.*.city": "required", + } + ); + expect(result.isValid).toBe(false); + expect(result.fields["users.*.addresses.*.city"]).toBe(false); + expect(result.errors["users.0.addresses.1.city"][0].message).toBe( + "The field is required." + ); + }); + + test("should validate everything", async () => { + const result = await validate( + { + secret: "some secret", + users: [ + { + addresses: [ + { + city: "New York", + }, + { + city: "Istanbul", + }, + ], + }, + { + addresses: [ + { + city: "New York", + }, + { + street: "Wall Street", + }, + ], + }, + ], + permissons: { + read: true, + write: true, + }, + }, + { + secret: "required|min:100", + "users.*.addresses.*.city": "required", + "permissons.read": "required|boolean", + "permissons.delete": "required|boolean", + } + ); + expect(result.isValid).toBe(false); + expect(result.fields.secret).toBe(false); + expect(result.fields["users.*.addresses.*.city"]).toBe(false); + expect(result.fields["permissons.read"]).toBe(true); + expect(result.fields["permissons.delete"]).toBe(false); + }); +});