Skip to content

Commit a21308e

Browse files
committed
Fixed #54 - Array of Object validation
1 parent 1716993 commit a21308e

File tree

9 files changed

+363
-63
lines changed

9 files changed

+363
-63
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release Notes
22

3+
## [2.2.0 (2025-01-13)](https://github.com/axe-api/axe-api/compare/2.2.0...2.2.1)
4+
5+
- Array and object validation [#54](https://github.com/axe-api/validator/issues/54)
6+
37
## [2.1.1 (2024-10-27)](https://github.com/axe-api/axe-api/compare/2.1.1...2.1.0)
48

59
- TypeError: list.map is not a function [#62](https://github.com/axe-api/validator/issues/62)

docs/getting-started.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,90 @@ By the example, you would get the following response:
8989
}
9090
}
9191
```
92+
93+
## Nested data validation
94+
95+
This feature allows dynamic traversal of nested data structures, supporting complex validation rules for paths like `users.*.addresses.*.city`.
96+
97+
It is inspired by Laravel's validation system and works seamlessly with arrays and objects, including deeply nested data.
98+
99+
```ts
100+
import { validate, setLocales, en } from "robust-validator";
101+
102+
setLocales(en);
103+
104+
const data = {
105+
secret: "some secret",
106+
users: [
107+
{
108+
addresses: [
109+
{
110+
city: "New York",
111+
},
112+
{
113+
city: "Istanbul",
114+
},
115+
],
116+
},
117+
{
118+
addresses: [
119+
{
120+
city: "New York",
121+
},
122+
{
123+
street: "Wall Street",
124+
},
125+
],
126+
},
127+
],
128+
permissons: {
129+
read: true,
130+
write: true,
131+
},
132+
};
133+
134+
const definition = {
135+
secret: "required|min:100",
136+
"users.*.addresses.*.city": "required",
137+
"permissons.read": "required|boolean",
138+
"permissons.delete": "required|boolean",
139+
};
140+
141+
const result = await validate(data, definition);
142+
console.log(result);
143+
```
144+
145+
And this is the content of the `result` variable:
146+
147+
```json
148+
{
149+
"isValid": false,
150+
"isInvalid": true,
151+
"fields": {
152+
"secret": false,
153+
"users.*.addresses.*.city": false,
154+
"permissons.read": true,
155+
"permissons.delete": false
156+
},
157+
"errors": {
158+
"secret": [
159+
{
160+
"rule": "min",
161+
"message": "The field must be at least 100."
162+
}
163+
],
164+
"users.1.addresses.1.city": [
165+
{
166+
"rule": "required",
167+
"message": "The field is required."
168+
}
169+
],
170+
"permissons.delete": [
171+
{
172+
"rule": "required",
173+
"message": "The field is required."
174+
}
175+
]
176+
}
177+
}
178+
```

docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ features:
1515
- title: Declarative ✍🏽
1616
details: Declarative rule definition allows you to save your rules in different places such as configuration files, databases, etc.
1717
- title: Simple 🐤
18-
details: Starting to validate data is very fast instead of creating complicated validation rules. You just need seconds.
18+
details: Starting to validate data is very fast. Instead of creating complicated validation rules, you just need seconds.
1919
- title: Proof of work 💪
2020
details: Laravel-ish data validation rules are well-tested as a concept. This library is just another implementation for JavaScript.
2121
- title: i18n 🇺🇳
22-
details: Multi-language error messages are supported internally, unlike other libraries. It provides consistency.
22+
details: Multi-language error messages are supported internally, unlike other libraries.
2323
---

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "robust-validator",
3-
"version": "2.1.1",
3+
"version": "2.2.0",
44
"description": "Rule-based data validation library",
55
"type": "module",
66
"main": "dist/index.cjs",

src/Interface.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,14 @@ export interface ILocale {
4444
key: LanguageType;
4545
values: Translation;
4646
}
47+
48+
export interface ITraversePair {
49+
path: string;
50+
value: any;
51+
}
52+
53+
export interface ITraverseItem {
54+
path: string;
55+
rules: string | string[];
56+
resolved: Array<ITraversePair>;
57+
}

src/helpers/validate.ts

Lines changed: 118 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { IContext, IValidationOptions, IValidationResult } from "../Interface";
1+
import {
2+
IContext,
3+
ITraverseItem,
4+
IValidationOptions,
5+
IValidationResult,
6+
} from "../Interface";
27
import { getMessage } from "../Locale";
38
import { Definition, ValidationResult } from "../Types";
49
import { toRuleDefinition } from "../Factory";
5-
import { getValueViaPath } from "./getValueViaPath";
610
import { getOptions } from "../Options";
711

812
export const validate = async (
@@ -29,6 +33,65 @@ export const validate = async (
2933
};
3034
};
3135

36+
const toTraverseArray = (data: any, definition: Definition) => {
37+
function resolvePath(data: any, path: string) {
38+
const parts = path.split(".");
39+
const result: Array<{ path: string; value: any }> = [];
40+
41+
function traverse(
42+
current: any,
43+
index = 0,
44+
resolvedPath: Array<string | number> = []
45+
) {
46+
if (index >= parts.length) {
47+
result.push({ path: resolvedPath.join("."), value: current });
48+
return;
49+
}
50+
51+
const part = parts[index];
52+
53+
if (part === "*") {
54+
if (Array.isArray(current)) {
55+
current.forEach((item, i) => {
56+
traverse(item, index + 1, [...resolvedPath, i]);
57+
});
58+
} else if (current && typeof current === "object") {
59+
Object.keys(current).forEach((key) => {
60+
traverse(current[key], index + 1, [...resolvedPath, key]);
61+
});
62+
} else {
63+
result.push({
64+
path: [...resolvedPath, "*"].join("."),
65+
value: current,
66+
});
67+
}
68+
} else {
69+
if (current && typeof current === "object" && part in current) {
70+
traverse(current[part], index + 1, [...resolvedPath, part]);
71+
} else {
72+
result.push({
73+
path: [...resolvedPath, part].join("."),
74+
value: undefined,
75+
});
76+
}
77+
}
78+
}
79+
80+
traverse(data);
81+
return result;
82+
}
83+
84+
const checks: ITraverseItem[] = [];
85+
86+
// Example usage
87+
Object.entries(definition).forEach(([path, rules]) => {
88+
const resolved = resolvePath(data, path);
89+
checks.push({ path, rules, resolved });
90+
});
91+
92+
return checks;
93+
};
94+
3295
const getResults = async (
3396
data: any,
3497
definition: Definition,
@@ -38,69 +101,63 @@ const getResults = async (
38101
const fields: Record<string, boolean> = {};
39102
const results: ValidationResult = {};
40103

41-
// Checking all validations
42-
for (const field in definition) {
43-
fields[field] = true;
44-
// Parsing the rules
45-
const params = definition[field];
46-
let ruleGroup: string = "";
47-
if (Array.isArray(params)) {
48-
ruleGroup = params.join("|");
49-
} else {
50-
ruleGroup = params;
51-
}
104+
const traverse = toTraverseArray(data, definition);
52105

53-
const rules = toRuleNameArray(ruleGroup).map(toRuleDefinition);
106+
for (const item of traverse) {
107+
const { path, rules, resolved } = item;
108+
fields[path] = true;
54109

55-
// Getting the value by the path
56-
const value = getValueViaPath(data, field);
110+
const rulesAsString = Array.isArray(rules) ? rules.join("|") : rules;
111+
112+
const ruleDefinitions =
113+
toRuleNameArray(rulesAsString).map(toRuleDefinition);
57114

58115
const context: IContext = {
59116
data,
60-
field,
61-
definition: ruleGroup,
117+
field: path,
118+
definition: rulesAsString,
62119
};
63120

64-
// Checking all rules one by one
65-
for (const rule of rules) {
66-
// If the value is empty but the rule is not required, we don't execute
67-
// the rules
68-
if (rule.name !== "required" && (value === null || value === undefined)) {
69-
continue;
70-
}
71-
72-
// Calling the rule function with the validation parameters
73-
const isRuleValid = await rule.callback(
74-
value,
75-
...[...rule.params, context]
76-
);
77-
78-
// Is the value valid?
79-
if (isRuleValid === false) {
80-
if (!results[field]) {
81-
results[field] = [];
121+
for (const check of resolved) {
122+
// Checking all rules one by one
123+
for (const rule of ruleDefinitions) {
124+
// If the value is empty but the rule is not required, we don't execute
125+
// the rules
126+
if (
127+
rule.name !== "required" &&
128+
(check.value === null || check.value === undefined)
129+
) {
130+
continue;
82131
}
83-
84-
isValid = false;
85-
fields[field] = false;
86-
87-
// Setting the rule and the error message
88-
results[field].push({
89-
rule: rule.name,
90-
message: getMessage(
91-
rule.name,
92-
rule.params,
93-
options.language,
94-
options.translations || {}
95-
),
96-
});
97-
98-
if (options.stopOnFail) {
99-
return {
100-
isValid: false,
101-
fields,
102-
results,
103-
};
132+
// Calling the rule function with the validation parameters
133+
const isRuleValid = await rule.callback(
134+
check.value,
135+
...[...rule.params, context]
136+
);
137+
// Is the value valid?
138+
if (isRuleValid === false) {
139+
if (!results[check.path]) {
140+
results[check.path] = [];
141+
}
142+
isValid = false;
143+
fields[path] = false;
144+
// Setting the rule and the error message
145+
results[check.path].push({
146+
rule: rule.name,
147+
message: getMessage(
148+
rule.name,
149+
rule.params,
150+
options.language,
151+
options.translations || {}
152+
),
153+
});
154+
if (options.stopOnFail) {
155+
return {
156+
isValid: false,
157+
fields,
158+
results,
159+
};
160+
}
104161
}
105162
}
106163
}
@@ -114,5 +171,9 @@ const getResults = async (
114171
};
115172

116173
const toRuleNameArray = (rules: string): string[] => {
174+
if (Array.isArray(rules)) {
175+
return rules;
176+
}
177+
117178
return rules.split("|");
118179
};

tests/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const EXISTS_RULE_TRANSLATIONS = {
1919

2020
describe("validate() function ", () => {
2121
beforeAll(async () => {
22-
await setLocales(en as ILocale);
22+
setLocales(en as ILocale);
2323
});
2424

2525
test("should be able to validate the general structure", async () => {

0 commit comments

Comments
 (0)