@zudojs/validation 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -3
- package/dist/validationComposer/validationComposer.combinators.js +4 -10
- package/dist/validationConstraints/collection/validationConstraints.array.js +14 -2
- package/dist/validationConstraints/scalar/validationConstraints.string.d.ts +3 -0
- package/dist/validationConstraints/scalar/validationConstraints.string.js +6 -1
- package/dist/validationConstraints/structure/validationConstraints.children.d.ts +15 -0
- package/dist/validationConstraints/structure/validationConstraints.children.js +41 -0
- package/dist/validationConstraints/structure/validationConstraints.size.js +20 -0
- package/dist/validationConstraints/structure/validationConstraints.traverse.d.ts +11 -9
- package/dist/validationConstraints/structure/validationConstraints.traverse.js +27 -40
- package/dist/validationConstraints/validationConstraints.base.js +14 -1
- package/dist/validationErrors/validationError.base.d.ts +18 -6
- package/dist/validationErrors/validationError.base.js +19 -8
- package/dist/validationRegistry/validationRegistry.core.d.ts +3 -1
- package/dist/validationRegistry/validationRegistry.core.js +14 -5
- package/dist/validationResult/validationResult.type.d.ts +8 -4
- package/dist/validationResult/validationResult.type.js +8 -6
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Schema validation with Zod integration, constraints, parsers, composers, circular detection, and depth/size checks.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-validation](https://zudojs.oyinlola.site/docs/packages-validation) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-validation.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -51,9 +57,21 @@ assertNoCircularReference(body);
|
|
|
51
57
|
`test()` stateful and flip the answer on alternate calls.
|
|
52
58
|
- Constraints carry an optional type guard, so wrong-typed input at a trust
|
|
53
59
|
boundary reports as a validation failure rather than a `TypeError`.
|
|
54
|
-
- The
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
- The size guard counts a shared subtree once per occurrence, the way a
|
|
61
|
+
serializer expands it, and measures what `toJSON()` returns when a value has
|
|
62
|
+
one. The cycle and depth guards walk a shared subtree once, so a small graph
|
|
63
|
+
of shared nodes cannot cost exponential time. All guards walk iteratively, so
|
|
64
|
+
deeply nested input cannot exhaust the stack inside the check, and handle
|
|
65
|
+
sparse arrays (a hole counts as `undefined`).
|
|
66
|
+
- `ValidationError` and `ValidationResultError` extend `@zudojs/errors`'
|
|
67
|
+
`ValidationError`, so `instanceof` and `isValidationError()` from either
|
|
68
|
+
package catch them. Their `toJSON()` keeps the base class's redaction:
|
|
69
|
+
submitted issue values and sensitive `context` keys never reach the JSON.
|
|
70
|
+
- `not(constraint)` fails closed: a wrong-typed value, or one that makes the
|
|
71
|
+
inner constraint throw, fails. `everyItem`/`someItem` read every index,
|
|
72
|
+
holes included.
|
|
73
|
+
- A registry rule that declares both `schema` and `constraints` runs the schema,
|
|
74
|
+
then the constraints on the parsed value.
|
|
57
75
|
|
|
58
76
|
## Features
|
|
59
77
|
|
|
@@ -34,16 +34,10 @@ export function any(...validators) {
|
|
|
34
34
|
return result;
|
|
35
35
|
issues.push(...result.issues);
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
path: [],
|
|
42
|
-
code: "no_validator_succeeded",
|
|
43
|
-
message: "No validation rule accepted the value.",
|
|
44
|
-
received: value,
|
|
45
|
-
},
|
|
46
|
-
]);
|
|
37
|
+
// Never echo the rejected value: issues flow into a 400 response and
|
|
38
|
+
// into logs, so `received` would hand back the password or token that
|
|
39
|
+
// was just refused.
|
|
40
|
+
return failure(issues.length > 0 ? issues : [noValidatorSucceeded()]);
|
|
47
41
|
};
|
|
48
42
|
}
|
|
49
43
|
/**
|
|
@@ -14,6 +14,18 @@ function itemHolds(constraint, value) {
|
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether any item's constraint result equals `want`, reading every index.
|
|
19
|
+
* `Array.prototype.every`/`some` skip holes, so `new Array(3)` passed any
|
|
20
|
+
* `everyItem` constraint.
|
|
21
|
+
*/
|
|
22
|
+
function scanItems(values, constraint, want) {
|
|
23
|
+
for (let i = 0; i < values.length; i++) {
|
|
24
|
+
if (itemHolds(constraint, values[i]) === want)
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
17
29
|
/**
|
|
18
30
|
* Requires an array to contain at least a given number of items.
|
|
19
31
|
*/
|
|
@@ -54,7 +66,7 @@ export function exactItems(length) {
|
|
|
54
66
|
* Requires every array item to satisfy a constraint.
|
|
55
67
|
*/
|
|
56
68
|
export function everyItem(constraint) {
|
|
57
|
-
return createConstraint((values) => values
|
|
69
|
+
return createConstraint((values) => !scanItems(values, constraint, false), {
|
|
58
70
|
name: `every_${constraint.name}`,
|
|
59
71
|
code: "item_constraint_failed",
|
|
60
72
|
message: constraint.message,
|
|
@@ -65,7 +77,7 @@ export function everyItem(constraint) {
|
|
|
65
77
|
* Requires at least one array item to satisfy a constraint.
|
|
66
78
|
*/
|
|
67
79
|
export function someItem(constraint) {
|
|
68
|
-
return createConstraint((values) => values
|
|
80
|
+
return createConstraint((values) => scanItems(values, constraint, true), {
|
|
69
81
|
name: `some_${constraint.name}`,
|
|
70
82
|
code: "some_item_constraint_failed",
|
|
71
83
|
message: `At least one item must satisfy ${constraint.name}.`,
|
|
@@ -30,6 +30,9 @@ export declare function matches(pattern: RegExp, message?: string): ValidationCo
|
|
|
30
30
|
*
|
|
31
31
|
* Deliberately structural, not a full RFC 5322 parser: it rejects the shapes
|
|
32
32
|
* that are certainly wrong and leaves deliverability to a verification step.
|
|
33
|
+
* Uses `ValidationPattern.EMAIL` from `@zudojs/constants`, the monorepo's one
|
|
34
|
+
* acceptance set (shared with `isEmail` in `@zudojs/types`). The 254-character
|
|
35
|
+
* bound is checked first so an oversized value is rejected without a scan.
|
|
33
36
|
*/
|
|
34
37
|
export declare const email: ValidationConstraint<string>;
|
|
35
38
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValidationLength, ValidationPattern } from "@zudojs/constants";
|
|
1
2
|
import { createConstraint, assertNonNegativeInteger, } from "../validationConstraints.base.js";
|
|
2
3
|
/** Counts Unicode code points rather than UTF-16 code units. */
|
|
3
4
|
function characterLength(value) {
|
|
@@ -77,8 +78,12 @@ export function matches(pattern, message = "Value has an invalid format.") {
|
|
|
77
78
|
*
|
|
78
79
|
* Deliberately structural, not a full RFC 5322 parser: it rejects the shapes
|
|
79
80
|
* that are certainly wrong and leaves deliverability to a verification step.
|
|
81
|
+
* Uses `ValidationPattern.EMAIL` from `@zudojs/constants`, the monorepo's one
|
|
82
|
+
* acceptance set (shared with `isEmail` in `@zudojs/types`). The 254-character
|
|
83
|
+
* bound is checked first so an oversized value is rejected without a scan.
|
|
80
84
|
*/
|
|
81
|
-
export const email = createConstraint((value) =>
|
|
85
|
+
export const email = createConstraint((value) => value.length <= ValidationLength.EMAIL &&
|
|
86
|
+
ValidationPattern.EMAIL.test(value), {
|
|
82
87
|
name: "email",
|
|
83
88
|
code: "invalid_email",
|
|
84
89
|
message: "Value must be a valid email address.",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/validation — Child enumeration for the structural guards.
|
|
3
|
+
*/
|
|
4
|
+
/** Whether a value has children worth descending into. */
|
|
5
|
+
export declare function isContainer(value: unknown): value is object;
|
|
6
|
+
/**
|
|
7
|
+
* The child values of a container, as [pathSegment, value] pairs.
|
|
8
|
+
*
|
|
9
|
+
* Arrays are read by index. `Array.prototype.map` skips holes and returns a
|
|
10
|
+
* sparse result, so `[1, , 3]` used to yield an `undefined` pair that crashed
|
|
11
|
+
* every guard with a raw TypeError; a hole is now an `undefined` child, which
|
|
12
|
+
* is also how `JSON.stringify` writes it (as `null`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function childrenOf(value: object): Array<[string, unknown]>;
|
|
15
|
+
//# sourceMappingURL=validationConstraints.children.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/validation — Child enumeration for the structural guards.
|
|
3
|
+
*/
|
|
4
|
+
/** Whether a value has children worth descending into. */
|
|
5
|
+
export function isContainer(value) {
|
|
6
|
+
return (typeof value === "object" && value !== null && !ArrayBuffer.isView(value));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The child values of a container, as [pathSegment, value] pairs.
|
|
10
|
+
*
|
|
11
|
+
* Arrays are read by index. `Array.prototype.map` skips holes and returns a
|
|
12
|
+
* sparse result, so `[1, , 3]` used to yield an `undefined` pair that crashed
|
|
13
|
+
* every guard with a raw TypeError; a hole is now an `undefined` child, which
|
|
14
|
+
* is also how `JSON.stringify` writes it (as `null`).
|
|
15
|
+
*/
|
|
16
|
+
export function childrenOf(value) {
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
const children = [];
|
|
19
|
+
for (let index = 0; index < value.length; index++) {
|
|
20
|
+
children.push([`[${index}]`, value[index]]);
|
|
21
|
+
}
|
|
22
|
+
return children;
|
|
23
|
+
}
|
|
24
|
+
if (value instanceof Map) {
|
|
25
|
+
const children = [];
|
|
26
|
+
let index = 0;
|
|
27
|
+
for (const [key, entry] of value) {
|
|
28
|
+
children.push([`.key(${index})`, key], [`[${String(key)}]`, entry]);
|
|
29
|
+
index++;
|
|
30
|
+
}
|
|
31
|
+
return children;
|
|
32
|
+
}
|
|
33
|
+
if (value instanceof Set) {
|
|
34
|
+
return [...value].map((entry, index) => [`.item(${index})`, entry]);
|
|
35
|
+
}
|
|
36
|
+
if (value instanceof Date || value instanceof RegExp)
|
|
37
|
+
return [];
|
|
38
|
+
const record = value;
|
|
39
|
+
return Object.keys(record).map((key) => [`.${key}`, record[key]]);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=validationConstraints.children.js.map
|
|
@@ -43,6 +43,24 @@ function chargeFor(value) {
|
|
|
43
43
|
keys.reduce((total, key) => total + key.length + 4, 0) +
|
|
44
44
|
Math.max(0, keys.length - 1));
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* What `JSON.stringify` will actually write for a node: the result of its
|
|
48
|
+
* `toJSON()` when it has one. Measuring the object's own keys instead let a
|
|
49
|
+
* class whose `toJSON` returns megabytes estimate at a dozen bytes. Dates and
|
|
50
|
+
* binary views keep their existing flat and byte-length charges.
|
|
51
|
+
*/
|
|
52
|
+
function resolveToJson(value) {
|
|
53
|
+
if (typeof value !== "object" ||
|
|
54
|
+
value === null ||
|
|
55
|
+
value instanceof Date ||
|
|
56
|
+
ArrayBuffer.isView(value)) {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
const toJSON = value.toJSON;
|
|
60
|
+
return typeof toJSON === "function"
|
|
61
|
+
? toJSON.call(value, "")
|
|
62
|
+
: value;
|
|
63
|
+
}
|
|
46
64
|
/**
|
|
47
65
|
* Estimate the byte size of a value as JSON without allocating a string.
|
|
48
66
|
*
|
|
@@ -61,6 +79,7 @@ export function estimateSerializedSize(value, maxBytes = Number.POSITIVE_INFINIT
|
|
|
61
79
|
maxDepth: MAX_MEASURABLE_DEPTH,
|
|
62
80
|
maxCost: maxBytes,
|
|
63
81
|
charge: chargeFor,
|
|
82
|
+
resolve: resolveToJson,
|
|
64
83
|
}).cost;
|
|
65
84
|
}
|
|
66
85
|
catch (error) {
|
|
@@ -86,6 +105,7 @@ export function assertSizeWithinLimit(value, maxSize) {
|
|
|
86
105
|
maxDepth: MAX_MEASURABLE_DEPTH,
|
|
87
106
|
maxCost: maxSize,
|
|
88
107
|
charge: chargeFor,
|
|
108
|
+
resolve: resolveToJson,
|
|
89
109
|
});
|
|
90
110
|
}
|
|
91
111
|
catch (error) {
|
|
@@ -17,15 +17,12 @@
|
|
|
17
17
|
* exactly the deeply nested input the depth guard exists to reject, so the
|
|
18
18
|
* check would fail inside itself before it could report anything.
|
|
19
19
|
*/
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
readonly observed: number;
|
|
27
|
-
constructor(halt: TraversalHalt, path: string, observed: number);
|
|
28
|
-
}
|
|
20
|
+
/**
|
|
21
|
+
* The halt signal is owned by `@zudojs/errors` (round 10 VAL-05/CV-02) and
|
|
22
|
+
* re-exported here, so the depth, size and circular guards keep importing
|
|
23
|
+
* it from this module.
|
|
24
|
+
*/
|
|
25
|
+
export { TraversalLimitError, type TraversalHalt } from "@zudojs/errors";
|
|
29
26
|
/** What the caller wants from each node. */
|
|
30
27
|
export interface TraversalVisitor {
|
|
31
28
|
/** Maximum nesting depth to descend before halting. */
|
|
@@ -36,6 +33,11 @@ export interface TraversalVisitor {
|
|
|
36
33
|
readonly failOnCycle?: boolean;
|
|
37
34
|
/** Cost contributed by a single node, excluding its children. */
|
|
38
35
|
charge?(value: unknown): number;
|
|
36
|
+
/**
|
|
37
|
+
* Maps a node to the value actually walked in its place, e.g. the result
|
|
38
|
+
* of `toJSON()` when measuring what `JSON.stringify` will write.
|
|
39
|
+
*/
|
|
40
|
+
resolve?(value: unknown): unknown;
|
|
39
41
|
}
|
|
40
42
|
/** What a completed traversal observed. */
|
|
41
43
|
export interface TraversalReport {
|
|
@@ -17,45 +17,14 @@
|
|
|
17
17
|
* exactly the deeply nested input the depth guard exists to reject, so the
|
|
18
18
|
* check would fail inside itself before it could report anything.
|
|
19
19
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
this.path = path;
|
|
29
|
-
this.observed = observed;
|
|
30
|
-
this.name = "TraversalLimitError";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
/** Whether a value has children worth descending into. */
|
|
34
|
-
function isContainer(value) {
|
|
35
|
-
return (typeof value === "object" && value !== null && !ArrayBuffer.isView(value));
|
|
36
|
-
}
|
|
37
|
-
/** The child values of a container, as [pathSegment, value] pairs. */
|
|
38
|
-
function childrenOf(value) {
|
|
39
|
-
if (Array.isArray(value)) {
|
|
40
|
-
return value.map((child, index) => [`[${index}]`, child]);
|
|
41
|
-
}
|
|
42
|
-
if (value instanceof Map) {
|
|
43
|
-
const children = [];
|
|
44
|
-
let index = 0;
|
|
45
|
-
for (const [key, entry] of value) {
|
|
46
|
-
children.push([`.key(${index})`, key], [`[${String(key)}]`, entry]);
|
|
47
|
-
index++;
|
|
48
|
-
}
|
|
49
|
-
return children;
|
|
50
|
-
}
|
|
51
|
-
if (value instanceof Set) {
|
|
52
|
-
return [...value].map((entry, index) => [`.item(${index})`, entry]);
|
|
53
|
-
}
|
|
54
|
-
if (value instanceof Date || value instanceof RegExp)
|
|
55
|
-
return [];
|
|
56
|
-
const record = value;
|
|
57
|
-
return Object.keys(record).map((key) => [`.${key}`, record[key]]);
|
|
58
|
-
}
|
|
20
|
+
import { TraversalLimitError } from "@zudojs/errors";
|
|
21
|
+
import { childrenOf, isContainer } from "./validationConstraints.children.js";
|
|
22
|
+
/**
|
|
23
|
+
* The halt signal is owned by `@zudojs/errors` (round 10 VAL-05/CV-02) and
|
|
24
|
+
* re-exported here, so the depth, size and circular guards keep importing
|
|
25
|
+
* it from this module.
|
|
26
|
+
*/
|
|
27
|
+
export { TraversalLimitError } from "@zudojs/errors";
|
|
59
28
|
/**
|
|
60
29
|
* Walk a value graph within explicit depth and cost bounds.
|
|
61
30
|
*
|
|
@@ -68,6 +37,12 @@ function childrenOf(value) {
|
|
|
68
37
|
export function traverse(root, visitor, rootPath = "root") {
|
|
69
38
|
const maxCost = visitor.maxCost ?? Number.POSITIVE_INFINITY;
|
|
70
39
|
const onPath = new Set();
|
|
40
|
+
// Without a per-node charge, a subtree already walked from some depth need
|
|
41
|
+
// not be walked again from the same depth or a shallower one: it holds no
|
|
42
|
+
// cycle (the walk would have halted) and cannot reach deeper than before.
|
|
43
|
+
// Re-walking it made a DAG of n shared `[node, node]` pairs cost 2^n.
|
|
44
|
+
// Only cost accounting must expand every occurrence, as a serializer does.
|
|
45
|
+
const walkedAt = visitor.charge ? undefined : new Map();
|
|
71
46
|
const stack = [{ value: root, depth: 0, path: rootPath }];
|
|
72
47
|
let cost = 0;
|
|
73
48
|
let deepest = 0;
|
|
@@ -77,7 +52,8 @@ export function traverse(root, visitor, rootPath = "root") {
|
|
|
77
52
|
onPath.delete(frame.leave);
|
|
78
53
|
continue;
|
|
79
54
|
}
|
|
80
|
-
const {
|
|
55
|
+
const { depth, path } = frame;
|
|
56
|
+
const value = visitor.resolve ? visitor.resolve(frame.value) : frame.value;
|
|
81
57
|
cost += visitor.charge?.(value) ?? 0;
|
|
82
58
|
if (cost > maxCost)
|
|
83
59
|
throw new TraversalLimitError("budget", path, cost);
|
|
@@ -85,15 +61,26 @@ export function traverse(root, visitor, rootPath = "root") {
|
|
|
85
61
|
deepest = depth;
|
|
86
62
|
if (!isContainer(value))
|
|
87
63
|
continue;
|
|
64
|
+
// A container occupies the level below the one it sits at, so an empty
|
|
65
|
+
// `{}` at depth d reaches d + 1 — the same level at which the depth
|
|
66
|
+
// guard below refuses to descend into it. Reporting only leaf depths
|
|
67
|
+
// made `getSerializationDepth({})` 0 while `assertDepthWithinLimit({},
|
|
68
|
+
// 0)` threw.
|
|
69
|
+
if (depth + 1 > deepest)
|
|
70
|
+
deepest = depth + 1;
|
|
88
71
|
if (onPath.has(value)) {
|
|
89
72
|
if (visitor.failOnCycle) {
|
|
90
73
|
throw new TraversalLimitError("cycle", path, depth);
|
|
91
74
|
}
|
|
92
75
|
continue;
|
|
93
76
|
}
|
|
77
|
+
const previous = walkedAt?.get(value);
|
|
78
|
+
if (previous !== undefined && depth <= previous)
|
|
79
|
+
continue;
|
|
94
80
|
if (depth >= visitor.maxDepth) {
|
|
95
81
|
throw new TraversalLimitError("depth", path, depth + 1);
|
|
96
82
|
}
|
|
83
|
+
walkedAt?.set(value, depth);
|
|
97
84
|
onPath.add(value);
|
|
98
85
|
stack.push({ value: undefined, depth, path, leave: value });
|
|
99
86
|
const children = childrenOf(value);
|
|
@@ -88,10 +88,23 @@ export function combineConstraints(...constraints) {
|
|
|
88
88
|
* Creates a negated constraint.
|
|
89
89
|
*/
|
|
90
90
|
export function not(constraint, options = {}) {
|
|
91
|
-
|
|
91
|
+
// Negation must not fail open. A value of the wrong type, or one that makes
|
|
92
|
+
// the inner check throw, used to count as "does not satisfy" and pass, so
|
|
93
|
+
// `not(matches(/<script/))` accepted `["<script>"]`. The inner guard is
|
|
94
|
+
// carried over and a throw is a failure.
|
|
95
|
+
const guard = options.guard ?? constraint.guard;
|
|
96
|
+
return createConstraint((value) => {
|
|
97
|
+
try {
|
|
98
|
+
return !constraint.validate(value);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}, {
|
|
92
104
|
name: options.name ?? `not_${constraint.name}`,
|
|
93
105
|
code: options.code ?? "negated_constraint_failed",
|
|
94
106
|
message: options.message ?? `Value must not satisfy ${constraint.name}.`,
|
|
107
|
+
...(guard ? { guard } : {}),
|
|
95
108
|
});
|
|
96
109
|
}
|
|
97
110
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Base validation error and error codes.
|
|
3
3
|
*/
|
|
4
4
|
import type { ValidationIssue } from "../validationResult/validationResult.type.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ValidationError as SharedValidationError, ErrorCategory, ErrorSeverity, type ErrorMetadata } from "@zudojs/errors";
|
|
6
6
|
/** Error codes used by the validation package. */
|
|
7
7
|
export declare enum ValidationErrorCode {
|
|
8
8
|
INVALID_INPUT = "VALIDATION_INVALID_INPUT",
|
|
@@ -20,8 +20,15 @@ export interface ValidationErrorOptions {
|
|
|
20
20
|
readonly cause?: unknown;
|
|
21
21
|
readonly context?: Readonly<Record<string, unknown>>;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Base error for validation failures.
|
|
25
|
+
*
|
|
26
|
+
* A thin subclass of `@zudojs/errors`' `ValidationError`, so a consumer that
|
|
27
|
+
* catches the shared class (or calls its `isValidationError`) also catches
|
|
28
|
+
* errors thrown by this package. It used to extend `BaseError` directly and
|
|
29
|
+
* was an unrelated class with the same name.
|
|
30
|
+
*/
|
|
31
|
+
export declare class ValidationError extends SharedValidationError {
|
|
25
32
|
readonly validationCode: ValidationErrorCode;
|
|
26
33
|
readonly issues: readonly ValidationIssue[];
|
|
27
34
|
readonly context?: Readonly<Record<string, unknown>>;
|
|
@@ -31,6 +38,11 @@ export declare class ValidationError extends BaseError {
|
|
|
31
38
|
get fieldErrors(): Readonly<Record<string, string>>;
|
|
32
39
|
/** Returns a formatted representation of all issues. */
|
|
33
40
|
get formattedIssues(): string;
|
|
41
|
+
/**
|
|
42
|
+
* Serializes the error. `issues` and `context` come from the base
|
|
43
|
+
* `toJSON()`, which redacts submitted issue values and sensitive metadata
|
|
44
|
+
* keys; re-adding the raw fields here used to undo that redaction.
|
|
45
|
+
*/
|
|
34
46
|
toJSON(): {
|
|
35
47
|
code: string;
|
|
36
48
|
category: ErrorCategory;
|
|
@@ -38,14 +50,14 @@ export declare class ValidationError extends BaseError {
|
|
|
38
50
|
statusCode: number;
|
|
39
51
|
expose: boolean;
|
|
40
52
|
isOperational: boolean;
|
|
41
|
-
metadata: Readonly<ErrorMetadata>;
|
|
53
|
+
metadata: Readonly<import("@zudojs/errors").ErrorMetadata>;
|
|
42
54
|
stack?: string;
|
|
43
55
|
cause?: import("@zudojs/errors").SerializedBaseError | unknown;
|
|
56
|
+
issues: readonly import("@zudojs/errors").ValidationIssue[];
|
|
44
57
|
name: string;
|
|
45
58
|
validationCode: ValidationErrorCode;
|
|
46
59
|
message: string;
|
|
47
|
-
|
|
48
|
-
context?: Readonly<Record<string, unknown>> | undefined;
|
|
60
|
+
context?: Readonly<ErrorMetadata> | undefined;
|
|
49
61
|
timestamp: number;
|
|
50
62
|
};
|
|
51
63
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Base validation error and error codes.
|
|
3
3
|
*/
|
|
4
4
|
import { formatIssues, toFieldErrors, } from "../validationResult/validationResult.type.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ValidationError as SharedValidationError, ErrorCode, ErrorCategory, ErrorSeverity, } from "@zudojs/errors";
|
|
6
6
|
/**
|
|
7
7
|
* Maps a package-specific validation code to the shared error registry code.
|
|
8
8
|
*
|
|
@@ -42,10 +42,16 @@ export var ValidationErrorCode;
|
|
|
42
42
|
ValidationErrorCode["SCHEMA_FAILED"] = "VALIDATION_SCHEMA_FAILED";
|
|
43
43
|
ValidationErrorCode["UNKNOWN"] = "VALIDATION_UNKNOWN";
|
|
44
44
|
})(ValidationErrorCode || (ValidationErrorCode = {}));
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Base error for validation failures.
|
|
47
|
+
*
|
|
48
|
+
* A thin subclass of `@zudojs/errors`' `ValidationError`, so a consumer that
|
|
49
|
+
* catches the shared class (or calls its `isValidationError`) also catches
|
|
50
|
+
* errors thrown by this package. It used to extend `BaseError` directly and
|
|
51
|
+
* was an unrelated class with the same name.
|
|
52
|
+
*/
|
|
53
|
+
export class ValidationError extends SharedValidationError {
|
|
47
54
|
validationCode;
|
|
48
|
-
issues;
|
|
49
55
|
context;
|
|
50
56
|
timestamp;
|
|
51
57
|
constructor(message, issues = [], options = {}) {
|
|
@@ -57,10 +63,10 @@ export class ValidationError extends BaseError {
|
|
|
57
63
|
expose: true,
|
|
58
64
|
cause: options.cause,
|
|
59
65
|
metadata: { ...options.context },
|
|
66
|
+
issues,
|
|
60
67
|
});
|
|
61
68
|
this.name = "ValidationError";
|
|
62
69
|
this.validationCode = options.code ?? ValidationErrorCode.UNKNOWN;
|
|
63
|
-
this.issues = Object.freeze([...issues]);
|
|
64
70
|
this.context = options.context;
|
|
65
71
|
this.timestamp = Date.now();
|
|
66
72
|
}
|
|
@@ -72,14 +78,19 @@ export class ValidationError extends BaseError {
|
|
|
72
78
|
get formattedIssues() {
|
|
73
79
|
return formatIssues(this.issues);
|
|
74
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Serializes the error. `issues` and `context` come from the base
|
|
83
|
+
* `toJSON()`, which redacts submitted issue values and sensitive metadata
|
|
84
|
+
* keys; re-adding the raw fields here used to undo that redaction.
|
|
85
|
+
*/
|
|
75
86
|
toJSON() {
|
|
87
|
+
const base = super.toJSON();
|
|
76
88
|
return {
|
|
77
|
-
...
|
|
89
|
+
...base,
|
|
78
90
|
name: this.name,
|
|
79
91
|
validationCode: this.validationCode,
|
|
80
92
|
message: this.message,
|
|
81
|
-
|
|
82
|
-
...(this.context ? { context: this.context } : {}),
|
|
93
|
+
...(this.context ? { context: base.metadata } : {}),
|
|
83
94
|
timestamp: this.timestamp,
|
|
84
95
|
};
|
|
85
96
|
}
|
|
@@ -39,7 +39,9 @@ export declare class ValidationRegistry {
|
|
|
39
39
|
* Constraints receive whatever the caller passed, which at a trust boundary
|
|
40
40
|
* is arbitrary JSON. `checkConstraints` guards and catches internally, so a
|
|
41
41
|
* wrong-typed value reports as a validation failure rather than escaping as
|
|
42
|
-
* a `TypeError` and turning a 400 into a 500.
|
|
42
|
+
* a `TypeError` and turning a 400 into a 500. A rule with both `schema`
|
|
43
|
+
* and `constraints` runs the schema first, then the constraints on its
|
|
44
|
+
* parsed output.
|
|
43
45
|
*/
|
|
44
46
|
validate<T>(name: string, value: unknown): ValidationResult<T>;
|
|
45
47
|
clone(): ValidationRegistry;
|
|
@@ -66,14 +66,23 @@ export class ValidationRegistry {
|
|
|
66
66
|
* Constraints receive whatever the caller passed, which at a trust boundary
|
|
67
67
|
* is arbitrary JSON. `checkConstraints` guards and catches internally, so a
|
|
68
68
|
* wrong-typed value reports as a validation failure rather than escaping as
|
|
69
|
-
* a `TypeError` and turning a 400 into a 500.
|
|
69
|
+
* a `TypeError` and turning a 400 into a 500. A rule with both `schema`
|
|
70
|
+
* and `constraints` runs the schema first, then the constraints on its
|
|
71
|
+
* parsed output.
|
|
70
72
|
*/
|
|
71
73
|
validate(name, value) {
|
|
72
74
|
const rule = this.require(name);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const constraints = rule.constraints ?? [];
|
|
76
|
+
// A rule may declare both. The constraints used to be skipped whenever a
|
|
77
|
+
// schema was present; now they run on the schema's parsed output.
|
|
78
|
+
if (rule.schema) {
|
|
79
|
+
const parsed = validate(rule.schema, value);
|
|
80
|
+
if (!parsed.success || constraints.length === 0)
|
|
81
|
+
return parsed;
|
|
82
|
+
return checkConstraints(constraints, parsed.data);
|
|
83
|
+
}
|
|
84
|
+
if (constraints.length > 0)
|
|
85
|
+
return checkConstraints(constraints, value);
|
|
77
86
|
throw new TypeError(`Validation rule "${name}" has no validation implementation.`);
|
|
78
87
|
}
|
|
79
88
|
clone() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validation result types and helpers.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { ValidationError as SharedValidationError, ErrorCategory, ErrorSeverity } from "@zudojs/errors";
|
|
5
5
|
/** A single validation issue. */
|
|
6
6
|
export interface ValidationIssue {
|
|
7
7
|
readonly path: readonly (string | number)[];
|
|
@@ -58,10 +58,14 @@ export declare function map<T, U>(result: ValidationResult<T>, fn: (data: T) =>
|
|
|
58
58
|
export declare function combine<T extends readonly unknown[]>(results: {
|
|
59
59
|
[K in keyof T]: ValidationResult<T[K]>;
|
|
60
60
|
}): ValidationResult<T>;
|
|
61
|
-
/**
|
|
62
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Error thrown when attempting to unwrap a failed validation result.
|
|
63
|
+
* Extends `@zudojs/errors`' `ValidationError`.
|
|
64
|
+
*/
|
|
65
|
+
export declare class ValidationResultError extends SharedValidationError {
|
|
63
66
|
readonly issues: readonly ValidationIssue[];
|
|
64
67
|
constructor(issues: readonly ValidationIssue[]);
|
|
68
|
+
/** Serializes the error; `issues` keep the base class's value redaction. */
|
|
65
69
|
toJSON(): {
|
|
66
70
|
code: string;
|
|
67
71
|
category: ErrorCategory;
|
|
@@ -72,9 +76,9 @@ export declare class ValidationResultError extends BaseError {
|
|
|
72
76
|
metadata: Readonly<import("@zudojs/errors").ErrorMetadata>;
|
|
73
77
|
stack?: string;
|
|
74
78
|
cause?: import("@zudojs/errors").SerializedBaseError | unknown;
|
|
79
|
+
issues: readonly import("@zudojs/errors").ValidationIssue[];
|
|
75
80
|
name: string;
|
|
76
81
|
message: string;
|
|
77
|
-
issues: readonly ValidationIssue[];
|
|
78
82
|
};
|
|
79
83
|
}
|
|
80
84
|
//# sourceMappingURL=validationResult.type.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validation result types and helpers.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { ValidationError as SharedValidationError, ErrorCode, ErrorCategory, ErrorSeverity, } from "@zudojs/errors";
|
|
5
5
|
/** Formats validation issues into a human-readable string. */
|
|
6
6
|
export function formatIssues(issues) {
|
|
7
7
|
if (issues.length === 0)
|
|
@@ -96,9 +96,11 @@ export function combine(results) {
|
|
|
96
96
|
? failure(failures)
|
|
97
97
|
: success(data);
|
|
98
98
|
}
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Error thrown when attempting to unwrap a failed validation result.
|
|
101
|
+
* Extends `@zudojs/errors`' `ValidationError`.
|
|
102
|
+
*/
|
|
103
|
+
export class ValidationResultError extends SharedValidationError {
|
|
102
104
|
constructor(issues) {
|
|
103
105
|
super(formatIssues(issues) || "Validation failed.", {
|
|
104
106
|
code: ErrorCode.VALIDATION_FAILED,
|
|
@@ -107,16 +109,16 @@ export class ValidationResultError extends BaseError {
|
|
|
107
109
|
statusCode: 400,
|
|
108
110
|
expose: true,
|
|
109
111
|
metadata: { issueCount: issues.length },
|
|
112
|
+
issues,
|
|
110
113
|
});
|
|
111
114
|
this.name = "ValidationResultError";
|
|
112
|
-
this.issues = Object.freeze([...issues]);
|
|
113
115
|
}
|
|
116
|
+
/** Serializes the error; `issues` keep the base class's value redaction. */
|
|
114
117
|
toJSON() {
|
|
115
118
|
return {
|
|
116
119
|
...super.toJSON(),
|
|
117
120
|
name: this.name,
|
|
118
121
|
message: this.message,
|
|
119
|
-
issues: this.issues,
|
|
120
122
|
};
|
|
121
123
|
}
|
|
122
124
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/validation",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Schema validation with Zod integration, constraints, parsers, composers, circular detection, and depth/size checks.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -21,8 +25,9 @@
|
|
|
21
25
|
"!dist/.tsbuildinfo"
|
|
22
26
|
],
|
|
23
27
|
"dependencies": {
|
|
24
|
-
"
|
|
25
|
-
"@zudojs/errors": "1.
|
|
28
|
+
"@zudojs/constants": "1.1.0",
|
|
29
|
+
"@zudojs/errors": "1.1.0",
|
|
30
|
+
"zod": "^4.4.3"
|
|
26
31
|
},
|
|
27
32
|
"devDependencies": {
|
|
28
33
|
"typescript": "7.0.2",
|