@zudojs/validation 0.1.0 → 1.0.1
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/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/validationComposer/index.d.ts +3 -1
- package/dist/validationComposer/index.js +3 -1
- package/dist/validationComposer/validationComposer.combinators.d.ts +21 -0
- package/dist/validationComposer/validationComposer.combinators.js +78 -0
- package/dist/validationComposer/validationComposer.core.d.ts +10 -24
- package/dist/validationComposer/validationComposer.core.js +2 -139
- package/dist/validationComposer/validationComposer.wrappers.d.ts +38 -0
- package/dist/validationComposer/validationComposer.wrappers.js +96 -0
- package/dist/validationConstraints/collection/index.d.ts +8 -0
- package/dist/validationConstraints/collection/index.js +8 -0
- package/dist/validationConstraints/{validationConstraints.array.d.ts → collection/validationConstraints.array.d.ts} +1 -1
- package/dist/validationConstraints/{validationConstraints.array.js → collection/validationConstraints.array.js} +23 -3
- package/dist/validationConstraints/{validationConstraints.collection.d.ts → collection/validationConstraints.collection.d.ts} +1 -1
- package/dist/validationConstraints/{validationConstraints.collection.js → collection/validationConstraints.collection.js} +1 -1
- package/dist/validationConstraints/index.d.ts +3 -8
- package/dist/validationConstraints/index.js +6 -16
- package/dist/validationConstraints/scalar/index.d.ts +9 -0
- package/dist/validationConstraints/scalar/index.js +9 -0
- package/dist/validationConstraints/scalar/validationConstraints.date.d.ts +13 -0
- package/dist/validationConstraints/scalar/validationConstraints.date.js +45 -0
- package/dist/validationConstraints/{validationConstraints.number.d.ts → scalar/validationConstraints.number.d.ts} +1 -1
- package/dist/validationConstraints/{validationConstraints.number.js → scalar/validationConstraints.number.js} +12 -1
- package/dist/validationConstraints/{validationConstraints.string.d.ts → scalar/validationConstraints.string.d.ts} +18 -3
- package/dist/validationConstraints/{validationConstraints.string.js → scalar/validationConstraints.string.js} +49 -13
- package/dist/validationConstraints/structure/index.d.ts +10 -0
- package/dist/validationConstraints/structure/index.js +10 -0
- package/dist/validationConstraints/{validationConstraints.circular.d.ts → structure/validationConstraints.circular.d.ts} +8 -4
- package/dist/validationConstraints/structure/validationConstraints.circular.js +55 -0
- package/dist/validationConstraints/structure/validationConstraints.depth.d.ts +37 -0
- package/dist/validationConstraints/structure/validationConstraints.depth.js +58 -0
- package/dist/validationConstraints/structure/validationConstraints.size.d.ts +31 -0
- package/dist/validationConstraints/structure/validationConstraints.size.js +101 -0
- package/dist/validationConstraints/structure/validationConstraints.traverse.d.ts +57 -0
- package/dist/validationConstraints/structure/validationConstraints.traverse.js +118 -0
- package/dist/validationConstraints/validationConstraints.base.d.ts +18 -3
- package/dist/validationConstraints/validationConstraints.base.js +50 -25
- package/dist/validationErrors/validationError.base.js +28 -1
- package/dist/validationFactory/validationFactory.core.d.ts +9 -8
- package/dist/validationFactory/validationFactory.core.js +11 -9
- package/dist/validationNormalizer/validationNormalizer.core.js +2 -4
- package/dist/validationNormalizer/validationNormalizer.helpers.d.ts +36 -3
- package/dist/validationNormalizer/validationNormalizer.helpers.js +48 -7
- package/dist/validationParser/index.d.ts +2 -1
- package/dist/validationParser/index.js +2 -1
- package/dist/validationParser/validationParser.collection.d.ts +28 -0
- package/dist/validationParser/validationParser.collection.js +98 -0
- package/dist/validationParser/validationParser.core.d.ts +0 -12
- package/dist/validationParser/validationParser.core.js +0 -71
- package/dist/validationRegistry/validationRegistry.core.d.ts +8 -0
- package/dist/validationRegistry/validationRegistry.core.js +15 -1
- package/dist/validationResult/validationResult.type.d.ts +8 -1
- package/dist/validationResult/validationResult.type.js +20 -5
- package/dist/validationTransformer/index.d.ts +2 -1
- package/dist/validationTransformer/index.js +2 -1
- package/dist/validationTransformer/validationTransformer.core.d.ts +0 -15
- package/dist/validationTransformer/validationTransformer.core.js +5 -56
- package/dist/validationTransformer/validationTransformer.helpers.d.ts +23 -0
- package/dist/validationTransformer/validationTransformer.helpers.js +67 -0
- package/package.json +24 -13
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/validationComposer/index.d.ts.map +0 -1
- package/dist/validationComposer/index.js.map +0 -1
- package/dist/validationComposer/validationComposer.core.d.ts.map +0 -1
- package/dist/validationComposer/validationComposer.core.js.map +0 -1
- package/dist/validationConstraints/index.d.ts.map +0 -1
- package/dist/validationConstraints/index.js.map +0 -1
- package/dist/validationConstraints/validationConstraints.array.d.ts.map +0 -1
- package/dist/validationConstraints/validationConstraints.array.js.map +0 -1
- package/dist/validationConstraints/validationConstraints.base.d.ts.map +0 -1
- package/dist/validationConstraints/validationConstraints.base.js.map +0 -1
- package/dist/validationConstraints/validationConstraints.circular.d.ts.map +0 -1
- package/dist/validationConstraints/validationConstraints.circular.js +0 -73
- package/dist/validationConstraints/validationConstraints.circular.js.map +0 -1
- package/dist/validationConstraints/validationConstraints.collection.d.ts.map +0 -1
- package/dist/validationConstraints/validationConstraints.collection.js.map +0 -1
- package/dist/validationConstraints/validationConstraints.date.d.ts +0 -14
- package/dist/validationConstraints/validationConstraints.date.d.ts.map +0 -1
- package/dist/validationConstraints/validationConstraints.date.js +0 -30
- package/dist/validationConstraints/validationConstraints.date.js.map +0 -1
- package/dist/validationConstraints/validationConstraints.depth.d.ts +0 -20
- package/dist/validationConstraints/validationConstraints.depth.d.ts.map +0 -1
- package/dist/validationConstraints/validationConstraints.depth.js +0 -70
- package/dist/validationConstraints/validationConstraints.depth.js.map +0 -1
- package/dist/validationConstraints/validationConstraints.number.d.ts.map +0 -1
- package/dist/validationConstraints/validationConstraints.number.js.map +0 -1
- package/dist/validationConstraints/validationConstraints.size.d.ts +0 -20
- package/dist/validationConstraints/validationConstraints.size.d.ts.map +0 -1
- package/dist/validationConstraints/validationConstraints.size.js +0 -82
- package/dist/validationConstraints/validationConstraints.size.js.map +0 -1
- package/dist/validationConstraints/validationConstraints.string.d.ts.map +0 -1
- package/dist/validationConstraints/validationConstraints.string.js.map +0 -1
- package/dist/validationErrors/index.d.ts.map +0 -1
- package/dist/validationErrors/index.js.map +0 -1
- package/dist/validationErrors/validationError.base.d.ts.map +0 -1
- package/dist/validationErrors/validationError.base.js.map +0 -1
- package/dist/validationErrors/validationError.types.d.ts.map +0 -1
- package/dist/validationErrors/validationError.types.js.map +0 -1
- package/dist/validationFactory/index.d.ts.map +0 -1
- package/dist/validationFactory/index.js.map +0 -1
- package/dist/validationFactory/validationFactory.core.d.ts.map +0 -1
- package/dist/validationFactory/validationFactory.core.js.map +0 -1
- package/dist/validationNormalizer/index.d.ts.map +0 -1
- package/dist/validationNormalizer/index.js.map +0 -1
- package/dist/validationNormalizer/validationNormalizer.core.d.ts.map +0 -1
- package/dist/validationNormalizer/validationNormalizer.core.js.map +0 -1
- package/dist/validationNormalizer/validationNormalizer.helpers.d.ts.map +0 -1
- package/dist/validationNormalizer/validationNormalizer.helpers.js.map +0 -1
- package/dist/validationParser/index.d.ts.map +0 -1
- package/dist/validationParser/index.js.map +0 -1
- package/dist/validationParser/validationParser.core.d.ts.map +0 -1
- package/dist/validationParser/validationParser.core.js.map +0 -1
- package/dist/validationRegistry/index.d.ts.map +0 -1
- package/dist/validationRegistry/index.js.map +0 -1
- package/dist/validationRegistry/validationRegistry.core.d.ts.map +0 -1
- package/dist/validationRegistry/validationRegistry.core.js.map +0 -1
- package/dist/validationRegistry/validationRegistry.readonly.d.ts.map +0 -1
- package/dist/validationRegistry/validationRegistry.readonly.js.map +0 -1
- package/dist/validationResult/index.d.ts.map +0 -1
- package/dist/validationResult/index.js.map +0 -1
- package/dist/validationResult/validationResult.type.d.ts.map +0 -1
- package/dist/validationResult/validationResult.type.js.map +0 -1
- package/dist/validationSchema/index.d.ts.map +0 -1
- package/dist/validationSchema/index.js.map +0 -1
- package/dist/validationSchema/validationSchema.core.d.ts.map +0 -1
- package/dist/validationSchema/validationSchema.core.js.map +0 -1
- package/dist/validationTransformer/index.d.ts.map +0 -1
- package/dist/validationTransformer/index.js.map +0 -1
- package/dist/validationTransformer/validationTransformer.core.d.ts.map +0 -1
- package/dist/validationTransformer/validationTransformer.core.js.map +0 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/validation — Size checking.
|
|
3
|
+
*
|
|
4
|
+
* Estimates serialized payload size to prevent memory exhaustion
|
|
5
|
+
* from excessively large payloads.
|
|
6
|
+
*/
|
|
7
|
+
import { SerializationPayloadTooLargeError } from "@zudojs/errors";
|
|
8
|
+
import { MAX_MEASURABLE_DEPTH } from "./validationConstraints.depth.js";
|
|
9
|
+
import { TraversalLimitError, traverse, } from "./validationConstraints.traverse.js";
|
|
10
|
+
/** Bytes charged for a single node, excluding its children. */
|
|
11
|
+
function chargeFor(value) {
|
|
12
|
+
if (value === null || value === undefined)
|
|
13
|
+
return 4;
|
|
14
|
+
switch (typeof value) {
|
|
15
|
+
case "string":
|
|
16
|
+
return value.length * 2 + 2;
|
|
17
|
+
case "number":
|
|
18
|
+
// JSON emits up to 21 characters for a double; charge the worst case
|
|
19
|
+
// rather than a flat 8, which under-counted every numeric field.
|
|
20
|
+
return 21;
|
|
21
|
+
case "boolean":
|
|
22
|
+
return 5;
|
|
23
|
+
case "bigint":
|
|
24
|
+
return value.toString().length + 2;
|
|
25
|
+
case "symbol":
|
|
26
|
+
case "function":
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (ArrayBuffer.isView(value))
|
|
30
|
+
return value.byteLength;
|
|
31
|
+
if (value instanceof Date)
|
|
32
|
+
return 26;
|
|
33
|
+
if (value instanceof RegExp)
|
|
34
|
+
return value.source.length + 4;
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return 2 + Math.max(0, value.length - 1);
|
|
37
|
+
if (value instanceof Map)
|
|
38
|
+
return 2 + Math.max(0, value.size * 3 - 1);
|
|
39
|
+
if (value instanceof Set)
|
|
40
|
+
return 2 + Math.max(0, value.size - 1);
|
|
41
|
+
const keys = Object.keys(value);
|
|
42
|
+
return (2 +
|
|
43
|
+
keys.reduce((total, key) => total + key.length + 4, 0) +
|
|
44
|
+
Math.max(0, keys.length - 1));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Estimate the byte size of a value as JSON without allocating a string.
|
|
48
|
+
*
|
|
49
|
+
* A value referenced from several places is charged once per occurrence, the
|
|
50
|
+
* way a serializer expands it. Counting it once let a compact payload built
|
|
51
|
+
* from shared subtrees estimate at a few hundred bytes while serializing to
|
|
52
|
+
* hundreds of megabytes.
|
|
53
|
+
*
|
|
54
|
+
* @param value - The value to measure.
|
|
55
|
+
* @param maxBytes - Stop counting past this budget; the budget is returned.
|
|
56
|
+
* @returns The estimated serialized size in bytes.
|
|
57
|
+
*/
|
|
58
|
+
export function estimateSerializedSize(value, maxBytes = Number.POSITIVE_INFINITY) {
|
|
59
|
+
try {
|
|
60
|
+
return traverse(value, {
|
|
61
|
+
maxDepth: MAX_MEASURABLE_DEPTH,
|
|
62
|
+
maxCost: maxBytes,
|
|
63
|
+
charge: chargeFor,
|
|
64
|
+
}).cost;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof TraversalLimitError) {
|
|
68
|
+
return error.halt === "budget" ? error.observed : maxBytes;
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Assert that a value's estimated serialized size is within limits.
|
|
75
|
+
*
|
|
76
|
+
* Aborts as soon as the budget is passed, so an oversized payload is rejected
|
|
77
|
+
* without first being measured in full.
|
|
78
|
+
*
|
|
79
|
+
* @param value - The value to check.
|
|
80
|
+
* @param maxSize - Maximum permitted size in bytes.
|
|
81
|
+
* @throws {SerializationPayloadTooLargeError} when the estimate exceeds maxSize.
|
|
82
|
+
*/
|
|
83
|
+
export function assertSizeWithinLimit(value, maxSize) {
|
|
84
|
+
try {
|
|
85
|
+
traverse(value, {
|
|
86
|
+
maxDepth: MAX_MEASURABLE_DEPTH,
|
|
87
|
+
maxCost: maxSize,
|
|
88
|
+
charge: chargeFor,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof TraversalLimitError && error.halt === "budget") {
|
|
93
|
+
throw new SerializationPayloadTooLargeError(error.observed, maxSize);
|
|
94
|
+
}
|
|
95
|
+
if (error instanceof TraversalLimitError && error.halt === "depth") {
|
|
96
|
+
throw new SerializationPayloadTooLargeError(error.observed, maxSize);
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=validationConstraints.size.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/validation — Bounded object-graph traversal.
|
|
3
|
+
*
|
|
4
|
+
* The depth, size and circular-reference guards all walk the same graph and
|
|
5
|
+
* all previously got the same three details wrong.
|
|
6
|
+
*
|
|
7
|
+
* The visited set must track the current *path*, not everything ever seen:
|
|
8
|
+
* marking on descent and unmarking on ascent detects a genuine cycle while
|
|
9
|
+
* still counting a shared subtree once per occurrence, the way a serializer
|
|
10
|
+
* would expand it.
|
|
11
|
+
*
|
|
12
|
+
* The walk must be bounded, and abort the moment a bound is passed. A guard
|
|
13
|
+
* that measures the whole input before comparing it to a limit is not a
|
|
14
|
+
* guard — it does the expensive work regardless.
|
|
15
|
+
*
|
|
16
|
+
* And the walk must be iterative. A recursive walker overflows the stack on
|
|
17
|
+
* exactly the deeply nested input the depth guard exists to reject, so the
|
|
18
|
+
* check would fail inside itself before it could report anything.
|
|
19
|
+
*/
|
|
20
|
+
/** Why a traversal stopped early. */
|
|
21
|
+
export type TraversalHalt = "depth" | "budget" | "cycle";
|
|
22
|
+
/** Signals that a traversal hit one of its bounds. */
|
|
23
|
+
export declare class TraversalLimitError extends Error {
|
|
24
|
+
readonly halt: TraversalHalt;
|
|
25
|
+
readonly path: string;
|
|
26
|
+
readonly observed: number;
|
|
27
|
+
constructor(halt: TraversalHalt, path: string, observed: number);
|
|
28
|
+
}
|
|
29
|
+
/** What the caller wants from each node. */
|
|
30
|
+
export interface TraversalVisitor {
|
|
31
|
+
/** Maximum nesting depth to descend before halting. */
|
|
32
|
+
readonly maxDepth: number;
|
|
33
|
+
/** Cumulative budget; halts once `charge` totals more than this. */
|
|
34
|
+
readonly maxCost?: number;
|
|
35
|
+
/** Whether a cycle should halt the walk rather than be skipped. */
|
|
36
|
+
readonly failOnCycle?: boolean;
|
|
37
|
+
/** Cost contributed by a single node, excluding its children. */
|
|
38
|
+
charge?(value: unknown): number;
|
|
39
|
+
}
|
|
40
|
+
/** What a completed traversal observed. */
|
|
41
|
+
export interface TraversalReport {
|
|
42
|
+
/** Deepest nesting level reached. */
|
|
43
|
+
readonly depth: number;
|
|
44
|
+
/** Total cost accumulated across every visited node. */
|
|
45
|
+
readonly cost: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Walk a value graph within explicit depth and cost bounds.
|
|
49
|
+
*
|
|
50
|
+
* @param root - The value to walk.
|
|
51
|
+
* @param visitor - Bounds and per-node cost.
|
|
52
|
+
* @param rootPath - Label for the root node in error paths.
|
|
53
|
+
* @returns What the traversal observed, when it completed within bounds.
|
|
54
|
+
* @throws {TraversalLimitError} as soon as a bound is exceeded.
|
|
55
|
+
*/
|
|
56
|
+
export declare function traverse(root: unknown, visitor: TraversalVisitor, rootPath?: string): TraversalReport;
|
|
57
|
+
//# sourceMappingURL=validationConstraints.traverse.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/validation — Bounded object-graph traversal.
|
|
3
|
+
*
|
|
4
|
+
* The depth, size and circular-reference guards all walk the same graph and
|
|
5
|
+
* all previously got the same three details wrong.
|
|
6
|
+
*
|
|
7
|
+
* The visited set must track the current *path*, not everything ever seen:
|
|
8
|
+
* marking on descent and unmarking on ascent detects a genuine cycle while
|
|
9
|
+
* still counting a shared subtree once per occurrence, the way a serializer
|
|
10
|
+
* would expand it.
|
|
11
|
+
*
|
|
12
|
+
* The walk must be bounded, and abort the moment a bound is passed. A guard
|
|
13
|
+
* that measures the whole input before comparing it to a limit is not a
|
|
14
|
+
* guard — it does the expensive work regardless.
|
|
15
|
+
*
|
|
16
|
+
* And the walk must be iterative. A recursive walker overflows the stack on
|
|
17
|
+
* exactly the deeply nested input the depth guard exists to reject, so the
|
|
18
|
+
* check would fail inside itself before it could report anything.
|
|
19
|
+
*/
|
|
20
|
+
/** Signals that a traversal hit one of its bounds. */
|
|
21
|
+
export class TraversalLimitError extends Error {
|
|
22
|
+
halt;
|
|
23
|
+
path;
|
|
24
|
+
observed;
|
|
25
|
+
constructor(halt, path, observed) {
|
|
26
|
+
super(`Traversal halted (${halt}) at ${path}`);
|
|
27
|
+
this.halt = halt;
|
|
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
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Walk a value graph within explicit depth and cost bounds.
|
|
61
|
+
*
|
|
62
|
+
* @param root - The value to walk.
|
|
63
|
+
* @param visitor - Bounds and per-node cost.
|
|
64
|
+
* @param rootPath - Label for the root node in error paths.
|
|
65
|
+
* @returns What the traversal observed, when it completed within bounds.
|
|
66
|
+
* @throws {TraversalLimitError} as soon as a bound is exceeded.
|
|
67
|
+
*/
|
|
68
|
+
export function traverse(root, visitor, rootPath = "root") {
|
|
69
|
+
const maxCost = visitor.maxCost ?? Number.POSITIVE_INFINITY;
|
|
70
|
+
const onPath = new Set();
|
|
71
|
+
const stack = [{ value: root, depth: 0, path: rootPath }];
|
|
72
|
+
let cost = 0;
|
|
73
|
+
let deepest = 0;
|
|
74
|
+
while (stack.length > 0) {
|
|
75
|
+
const frame = stack.pop();
|
|
76
|
+
if (frame.leave) {
|
|
77
|
+
onPath.delete(frame.leave);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const { value, depth, path } = frame;
|
|
81
|
+
cost += visitor.charge?.(value) ?? 0;
|
|
82
|
+
if (cost > maxCost)
|
|
83
|
+
throw new TraversalLimitError("budget", path, cost);
|
|
84
|
+
if (depth > deepest)
|
|
85
|
+
deepest = depth;
|
|
86
|
+
if (!isContainer(value))
|
|
87
|
+
continue;
|
|
88
|
+
// A container occupies the level below the one it sits at, so an empty
|
|
89
|
+
// `{}` at depth d reaches d + 1 — the same level at which the depth
|
|
90
|
+
// guard below refuses to descend into it. Reporting only leaf depths
|
|
91
|
+
// made `getSerializationDepth({})` 0 while `assertDepthWithinLimit({},
|
|
92
|
+
// 0)` threw.
|
|
93
|
+
if (depth + 1 > deepest)
|
|
94
|
+
deepest = depth + 1;
|
|
95
|
+
if (onPath.has(value)) {
|
|
96
|
+
if (visitor.failOnCycle) {
|
|
97
|
+
throw new TraversalLimitError("cycle", path, depth);
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (depth >= visitor.maxDepth) {
|
|
102
|
+
throw new TraversalLimitError("depth", path, depth + 1);
|
|
103
|
+
}
|
|
104
|
+
onPath.add(value);
|
|
105
|
+
stack.push({ value: undefined, depth, path, leave: value });
|
|
106
|
+
const children = childrenOf(value);
|
|
107
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
108
|
+
const [segment, child] = children[index];
|
|
109
|
+
stack.push({
|
|
110
|
+
value: child,
|
|
111
|
+
depth: depth + 1,
|
|
112
|
+
path: `${path}${segment}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { depth: deepest, cost };
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=validationConstraints.traverse.js.map
|
|
@@ -7,21 +7,36 @@ export interface ValidationConstraint<T> {
|
|
|
7
7
|
readonly validate: (value: T) => boolean;
|
|
8
8
|
readonly message: string;
|
|
9
9
|
readonly code: string;
|
|
10
|
+
/**
|
|
11
|
+
* Narrows an arbitrary value to the type `validate` expects.
|
|
12
|
+
*
|
|
13
|
+
* Constraints are frequently handed values straight off a trust boundary,
|
|
14
|
+
* where the declared type parameter guarantees nothing. Without a guard,
|
|
15
|
+
* `everyItem(...)` called on a number throws a raw `TypeError` out of the
|
|
16
|
+
* validator, turning a 400 into a 500.
|
|
17
|
+
*/
|
|
18
|
+
readonly guard?: (value: unknown) => value is T;
|
|
10
19
|
}
|
|
11
20
|
/**
|
|
12
21
|
* Options for creating a validation constraint.
|
|
13
22
|
*/
|
|
14
|
-
export interface ConstraintOptions {
|
|
23
|
+
export interface ConstraintOptions<T = unknown> {
|
|
15
24
|
readonly name?: string;
|
|
16
25
|
readonly code?: string;
|
|
17
26
|
readonly message?: string;
|
|
27
|
+
readonly guard?: (value: unknown) => value is T;
|
|
18
28
|
}
|
|
19
29
|
/**
|
|
20
30
|
* Creates a reusable validation constraint.
|
|
21
31
|
*/
|
|
22
|
-
export declare function createConstraint<T>(validate: (value: T) => boolean, options?: ConstraintOptions): ValidationConstraint<T>;
|
|
32
|
+
export declare function createConstraint<T>(validate: (value: T) => boolean, options?: ConstraintOptions<T>): ValidationConstraint<T>;
|
|
23
33
|
/**
|
|
24
34
|
* Executes a constraint against a value.
|
|
35
|
+
*
|
|
36
|
+
* The rejected value is deliberately *not* attached to the issue. Issues flow
|
|
37
|
+
* into `ValidationError`, which is exposed with a 400, so echoing the input
|
|
38
|
+
* would return rejected passwords, tokens and PII to the caller and write them
|
|
39
|
+
* to any log that serializes the error.
|
|
25
40
|
*/
|
|
26
41
|
export declare function checkConstraint<T>(constraint: ValidationConstraint<T>, value: T, path?: readonly (string | number)[]): ValidationResult<T>;
|
|
27
42
|
/**
|
|
@@ -35,7 +50,7 @@ export declare function combineConstraints<T>(...constraints: readonly Validatio
|
|
|
35
50
|
/**
|
|
36
51
|
* Creates a negated constraint.
|
|
37
52
|
*/
|
|
38
|
-
export declare function not<T>(constraint: ValidationConstraint<T>, options?: ConstraintOptions): ValidationConstraint<T>;
|
|
53
|
+
export declare function not<T>(constraint: ValidationConstraint<T>, options?: ConstraintOptions<T>): ValidationConstraint<T>;
|
|
39
54
|
/**
|
|
40
55
|
* Requires a value to be defined.
|
|
41
56
|
*/
|
|
@@ -3,30 +3,61 @@ import { failure, success, } from "../validationResult/validationResult.type.js"
|
|
|
3
3
|
* Creates a reusable validation constraint.
|
|
4
4
|
*/
|
|
5
5
|
export function createConstraint(validate, options = {}) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const guard = options.guard;
|
|
7
|
+
/**
|
|
8
|
+
* `validate` is public, so it is called directly as often as it is called
|
|
9
|
+
* through `checkConstraints`. Applying the guard here rather than only at
|
|
10
|
+
* the call site means no entry point can be handed a wrong-typed value and
|
|
11
|
+
* throw a raw `TypeError` back at a trust boundary.
|
|
12
|
+
*/
|
|
13
|
+
const guarded = guard
|
|
14
|
+
? (value) => guard(value) && validate(value)
|
|
15
|
+
: validate;
|
|
9
16
|
return Object.freeze({
|
|
10
|
-
name,
|
|
11
|
-
validate,
|
|
12
|
-
message,
|
|
13
|
-
code,
|
|
17
|
+
name: options.name ?? "custom",
|
|
18
|
+
validate: guarded,
|
|
19
|
+
message: options.message ?? "Validation constraint failed.",
|
|
20
|
+
code: options.code ?? "constraint_failed",
|
|
21
|
+
...(guard ? { guard } : {}),
|
|
14
22
|
});
|
|
15
23
|
}
|
|
16
24
|
/**
|
|
17
|
-
*
|
|
25
|
+
* Run a constraint against a value of unknown type.
|
|
26
|
+
*
|
|
27
|
+
* Returns false rather than throwing when the value is the wrong shape, so a
|
|
28
|
+
* mistyped field reports as a validation failure instead of escaping as an
|
|
29
|
+
* unhandled error.
|
|
18
30
|
*/
|
|
19
|
-
|
|
20
|
-
if (constraint.
|
|
21
|
-
return
|
|
31
|
+
function runConstraint(constraint, value) {
|
|
32
|
+
if (constraint.guard && !constraint.guard(value))
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
return constraint.validate(value);
|
|
22
36
|
}
|
|
23
|
-
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Builds the issue describing a failed constraint. */
|
|
42
|
+
function toIssue(constraint, path) {
|
|
43
|
+
return {
|
|
24
44
|
path: [...path],
|
|
25
45
|
code: constraint.code,
|
|
26
46
|
message: constraint.message,
|
|
27
|
-
received: value,
|
|
28
47
|
};
|
|
29
|
-
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Executes a constraint against a value.
|
|
51
|
+
*
|
|
52
|
+
* The rejected value is deliberately *not* attached to the issue. Issues flow
|
|
53
|
+
* into `ValidationError`, which is exposed with a 400, so echoing the input
|
|
54
|
+
* would return rejected passwords, tokens and PII to the caller and write them
|
|
55
|
+
* to any log that serializes the error.
|
|
56
|
+
*/
|
|
57
|
+
export function checkConstraint(constraint, value, path = []) {
|
|
58
|
+
if (runConstraint(constraint, value))
|
|
59
|
+
return success(value);
|
|
60
|
+
return failure([toIssue(constraint, path)]);
|
|
30
61
|
}
|
|
31
62
|
/**
|
|
32
63
|
* Executes multiple constraints against a value.
|
|
@@ -34,25 +65,19 @@ export function checkConstraint(constraint, value, path = []) {
|
|
|
34
65
|
export function checkConstraints(constraints, value, path = []) {
|
|
35
66
|
const issues = [];
|
|
36
67
|
for (const constraint of constraints) {
|
|
37
|
-
if (!constraint
|
|
38
|
-
issues.push(
|
|
39
|
-
path: [...path],
|
|
40
|
-
code: constraint.code,
|
|
41
|
-
message: constraint.message,
|
|
42
|
-
received: value,
|
|
43
|
-
});
|
|
68
|
+
if (!runConstraint(constraint, value)) {
|
|
69
|
+
issues.push(toIssue(constraint, path));
|
|
44
70
|
}
|
|
45
71
|
}
|
|
46
|
-
if (issues.length > 0)
|
|
72
|
+
if (issues.length > 0)
|
|
47
73
|
return failure(issues);
|
|
48
|
-
}
|
|
49
74
|
return success(value);
|
|
50
75
|
}
|
|
51
76
|
/**
|
|
52
77
|
* Combines constraints into a single constraint.
|
|
53
78
|
*/
|
|
54
79
|
export function combineConstraints(...constraints) {
|
|
55
|
-
return createConstraint((value) => constraints.every((constraint) => constraint
|
|
80
|
+
return createConstraint((value) => constraints.every((constraint) => runConstraint(constraint, value)), {
|
|
56
81
|
name: constraints.map((constraint) => constraint.name).join("_and_") ||
|
|
57
82
|
"combined",
|
|
58
83
|
code: "combined_constraint_failed",
|
|
@@ -63,7 +88,7 @@ export function combineConstraints(...constraints) {
|
|
|
63
88
|
* Creates a negated constraint.
|
|
64
89
|
*/
|
|
65
90
|
export function not(constraint, options = {}) {
|
|
66
|
-
return createConstraint((value) => !constraint
|
|
91
|
+
return createConstraint((value) => !runConstraint(constraint, value), {
|
|
67
92
|
name: options.name ?? `not_${constraint.name}`,
|
|
68
93
|
code: options.code ?? "negated_constraint_failed",
|
|
69
94
|
message: options.message ?? `Value must not satisfy ${constraint.name}.`,
|
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { formatIssues, toFieldErrors, } from "../validationResult/validationResult.type.js";
|
|
5
5
|
import { BaseError, ErrorCode, ErrorCategory, ErrorSeverity, } from "@zudojs/errors";
|
|
6
|
+
/**
|
|
7
|
+
* Maps a package-specific validation code to the shared error registry code.
|
|
8
|
+
*
|
|
9
|
+
* `ValidationErrorCode` and `ErrorCode` are different enums. Casting one into
|
|
10
|
+
* the other put a value on `BaseError.code` that the errors package does not
|
|
11
|
+
* recognise, so any status mapping or registry lookup keyed on it missed.
|
|
12
|
+
*/
|
|
13
|
+
function toErrorCode(code) {
|
|
14
|
+
switch (code) {
|
|
15
|
+
case ValidationErrorCode.REQUIRED:
|
|
16
|
+
return ErrorCode.MISSING_FIELD;
|
|
17
|
+
case ValidationErrorCode.INVALID_TYPE:
|
|
18
|
+
return ErrorCode.INVALID_FIELD;
|
|
19
|
+
case ValidationErrorCode.INVALID_FORMAT:
|
|
20
|
+
return ErrorCode.INVALID_FORMAT;
|
|
21
|
+
case ValidationErrorCode.INVALID_VALUE:
|
|
22
|
+
return ErrorCode.INVALID_VALUE;
|
|
23
|
+
case ValidationErrorCode.INVALID_INPUT:
|
|
24
|
+
return ErrorCode.INVALID_INPUT;
|
|
25
|
+
case ValidationErrorCode.SCHEMA_FAILED:
|
|
26
|
+
return ErrorCode.SCHEMA_VALIDATION;
|
|
27
|
+
case ValidationErrorCode.CONSTRAINT_FAILED:
|
|
28
|
+
case ValidationErrorCode.UNKNOWN:
|
|
29
|
+
default:
|
|
30
|
+
return ErrorCode.VALIDATION_FAILED;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
6
33
|
/** Error codes used by the validation package. */
|
|
7
34
|
export var ValidationErrorCode;
|
|
8
35
|
(function (ValidationErrorCode) {
|
|
@@ -23,7 +50,7 @@ export class ValidationError extends BaseError {
|
|
|
23
50
|
timestamp;
|
|
24
51
|
constructor(message, issues = [], options = {}) {
|
|
25
52
|
super(message, {
|
|
26
|
-
code: options.code
|
|
53
|
+
code: toErrorCode(options.code),
|
|
27
54
|
category: ErrorCategory.VALIDATION,
|
|
28
55
|
severity: ErrorSeverity.WARNING,
|
|
29
56
|
statusCode: 400,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ValidationConstraint } from "../validationConstraints/index.js";
|
|
1
|
+
import type { ConstraintOptions, ValidationConstraint } from "../validationConstraints/index.js";
|
|
2
2
|
import type { ValidationResult } from "../validationResult/validationResult.type.js";
|
|
3
3
|
import type { ValidationSchema } from "../validationSchema/validationSchema.core.js";
|
|
4
4
|
import { type ValidationParser, type AsyncValidationParser } from "../validationParser/validationParser.core.js";
|
|
@@ -21,11 +21,7 @@ export declare class ValidationFactory {
|
|
|
21
21
|
normalizer<T>(normalizer: Normalizer<T>): ValidationNormalizer<T>;
|
|
22
22
|
asyncNormalizer<T>(normalizer: AsyncNormalizer<T>): AsyncValidationNormalizer<T>;
|
|
23
23
|
composer<T>(steps: readonly ValidationStep<T>[], options?: ValidationComposerOptions): ValidationComposer<T>;
|
|
24
|
-
constraint<T>(validateValue: (value: T) => boolean, options?:
|
|
25
|
-
readonly name?: string;
|
|
26
|
-
readonly code?: string;
|
|
27
|
-
readonly message?: string;
|
|
28
|
-
}): ValidationConstraint<T>;
|
|
24
|
+
constraint<T>(validateValue: (value: T) => boolean, options?: ConstraintOptions<T>): ValidationConstraint<T>;
|
|
29
25
|
validate<T>(schema: ValidationSchema<T>, value: unknown): ValidationResult<T>;
|
|
30
26
|
validateAsync<T>(schema: ValidationSchema<T>, value: unknown): Promise<ValidationResult<T>>;
|
|
31
27
|
check<T>(constraints: readonly ValidationConstraint<T>[], value: T): ValidationResult<T>;
|
|
@@ -35,6 +31,11 @@ export declare class ValidationFactory {
|
|
|
35
31
|
}
|
|
36
32
|
/** Creates a new validation factory. */
|
|
37
33
|
export declare function createValidationFactory(options?: ValidationFactoryOptions): ValidationFactory;
|
|
38
|
-
/**
|
|
39
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Create a factory that shares a registry with an existing one.
|
|
36
|
+
*
|
|
37
|
+
* Prefer this to a process-wide singleton: a shared factory means one module
|
|
38
|
+
* calling `registry.clear()` silently removes rules another module registered.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createScopedValidationFactory(parent: ValidationFactory): ValidationFactory;
|
|
40
41
|
//# sourceMappingURL=validationFactory.core.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { checkConstraints } from "../validationConstraints/index.js";
|
|
1
|
+
import { checkConstraints, createConstraint, } from "../validationConstraints/index.js";
|
|
2
2
|
import { validate, validateAsync, } from "../validationSchema/validationSchema.core.js";
|
|
3
3
|
import { createValidationParser, createAsyncValidationParser, } from "../validationParser/validationParser.core.js";
|
|
4
4
|
import { createValidationTransformer, createAsyncValidationTransformer, } from "../validationTransformer/validationTransformer.core.js";
|
|
@@ -33,12 +33,7 @@ export class ValidationFactory {
|
|
|
33
33
|
return createValidationComposer(steps, options);
|
|
34
34
|
}
|
|
35
35
|
constraint(validateValue, options = {}) {
|
|
36
|
-
return
|
|
37
|
-
name: options.name ?? "custom",
|
|
38
|
-
code: options.code ?? "constraint_failed",
|
|
39
|
-
message: options.message ?? "Validation constraint failed.",
|
|
40
|
-
validate: validateValue,
|
|
41
|
-
};
|
|
36
|
+
return createConstraint(validateValue, options);
|
|
42
37
|
}
|
|
43
38
|
validate(schema, value) {
|
|
44
39
|
return validate(schema, value);
|
|
@@ -65,6 +60,13 @@ export class ValidationFactory {
|
|
|
65
60
|
export function createValidationFactory(options = {}) {
|
|
66
61
|
return new ValidationFactory(options);
|
|
67
62
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Create a factory that shares a registry with an existing one.
|
|
65
|
+
*
|
|
66
|
+
* Prefer this to a process-wide singleton: a shared factory means one module
|
|
67
|
+
* calling `registry.clear()` silently removes rules another module registered.
|
|
68
|
+
*/
|
|
69
|
+
export function createScopedValidationFactory(parent) {
|
|
70
|
+
return new ValidationFactory({ registry: parent.registry });
|
|
71
|
+
}
|
|
70
72
|
//# sourceMappingURL=validationFactory.core.js.map
|
|
@@ -17,13 +17,12 @@ export function createNormalizer(normalizer, options = {}) {
|
|
|
17
17
|
try {
|
|
18
18
|
return success(normalizer(value));
|
|
19
19
|
}
|
|
20
|
-
catch
|
|
20
|
+
catch {
|
|
21
21
|
return failure([
|
|
22
22
|
{
|
|
23
23
|
path: [],
|
|
24
24
|
code: "normalization_failed",
|
|
25
25
|
message: options.errorMessage ?? "Value normalization failed.",
|
|
26
|
-
received: error instanceof Error ? error.message : error,
|
|
27
26
|
},
|
|
28
27
|
]);
|
|
29
28
|
}
|
|
@@ -47,13 +46,12 @@ export function createAsyncNormalizer(normalizer, options = {}) {
|
|
|
47
46
|
try {
|
|
48
47
|
return success(await normalizer(value));
|
|
49
48
|
}
|
|
50
|
-
catch
|
|
49
|
+
catch {
|
|
51
50
|
return failure([
|
|
52
51
|
{
|
|
53
52
|
path: [],
|
|
54
53
|
code: "normalization_failed",
|
|
55
54
|
message: options.errorMessage ?? "Value normalization failed.",
|
|
56
|
-
received: error instanceof Error ? error.message : error,
|
|
57
55
|
},
|
|
58
56
|
]);
|
|
59
57
|
}
|
|
@@ -11,17 +11,50 @@ export declare function normalizeLowercase(value: string): string;
|
|
|
11
11
|
export declare function normalizeUppercase(value: string): string;
|
|
12
12
|
/** Normalizes Unicode text using NFC normalization. */
|
|
13
13
|
export declare function normalizeUnicode(value: string): string;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Normalizes Unicode text using NFKC, folding compatibility forms.
|
|
16
|
+
*
|
|
17
|
+
* Use this wherever the result identifies something. NFC is canonical only:
|
|
18
|
+
* it leaves ligatures and fullwidth forms distinct from their ASCII
|
|
19
|
+
* spellings, so two visually identical identifiers survive as two values.
|
|
20
|
+
*/
|
|
21
|
+
export declare function normalizeUnicodeCompatibility(value: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Case-folds a string for identifier comparison.
|
|
24
|
+
*
|
|
25
|
+
* `toLowerCase` is a locale-sensitive display transform. Folding through
|
|
26
|
+
* upper- then lower-case is the closest stable approximation available
|
|
27
|
+
* without ICU, and is what identifier comparison needs.
|
|
28
|
+
*/
|
|
29
|
+
export declare function foldCase(value: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Normalizes an email address.
|
|
32
|
+
*
|
|
33
|
+
* Only the domain is lowercased. The local part is case-sensitive per RFC
|
|
34
|
+
* 5321, and folding it can merge two distinct mailboxes.
|
|
35
|
+
*/
|
|
15
36
|
export declare function normalizeEmail(value: string): string;
|
|
16
37
|
/** Normalizes a URL by removing surrounding whitespace. */
|
|
17
38
|
export declare function normalizeUrl(value: string): string;
|
|
18
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes an identifier by trimming, compatibility-folding and case-folding.
|
|
41
|
+
*
|
|
42
|
+
* The output identifies an account or a resource, so it uses NFKC and a case
|
|
43
|
+
* fold rather than NFC and `toLowerCase`: otherwise two spellings that render
|
|
44
|
+
* identically normalize to two different identifiers.
|
|
45
|
+
*/
|
|
19
46
|
export declare function normalizeIdentifier(value: string): string;
|
|
20
47
|
/** Removes surrounding quotes from a string. */
|
|
21
48
|
export declare function normalizeQuotes(value: string): string;
|
|
22
49
|
/** Removes Unicode byte-order marks from the beginning of text. */
|
|
23
50
|
export declare function removeBom(value: string): string;
|
|
24
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Normalizes an array by applying a normalizer to every item.
|
|
53
|
+
*
|
|
54
|
+
* The normalizer is invoked with the value only. Passing it straight to `map`
|
|
55
|
+
* would also hand it the index and the array, which silently overrides the
|
|
56
|
+
* optional second parameter of functions like `parseInt`.
|
|
57
|
+
*/
|
|
25
58
|
export declare function normalizeArray<T>(values: readonly T[], normalizer: (value: T) => T): T[];
|
|
26
59
|
/** Normalizes an array asynchronously. */
|
|
27
60
|
export declare function normalizeArrayAsync<T>(values: readonly T[], normalizer: (value: T) => T | Promise<T>): Promise<T[]>;
|