@azure-net/node-kit 0.0.0-stage → 0.1.0
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +301 -2
- package/VALIDATION.md +866 -0
- package/dist/delivery/http/ResponseBuilder.d.ts +44 -0
- package/dist/delivery/http/ResponseBuilder.js +135 -0
- package/dist/delivery/http/index.d.ts +1 -0
- package/dist/delivery/http/index.js +1 -0
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.js +1 -0
- package/dist/domain/Ensure.d.ts +2 -0
- package/dist/domain/Ensure.js +7 -0
- package/dist/domain/EnsureError.d.ts +4 -0
- package/dist/domain/EnsureError.js +7 -0
- package/dist/domain/index.d.ts +2 -0
- package/dist/domain/index.js +2 -0
- package/dist/infrastructure/background/BackgroundTask.d.ts +12 -0
- package/dist/infrastructure/background/BackgroundTask.js +41 -0
- package/dist/infrastructure/background/index.d.ts +1 -0
- package/dist/infrastructure/background/index.js +1 -0
- package/dist/infrastructure/environment/Env.d.ts +19 -0
- package/dist/infrastructure/environment/Env.js +33 -0
- package/dist/infrastructure/environment/index.d.ts +1 -0
- package/dist/infrastructure/environment/index.js +1 -0
- package/dist/infrastructure/index.d.ts +2 -0
- package/dist/infrastructure/index.js +2 -0
- package/dist/tools/DateHelper.d.ts +13 -0
- package/dist/tools/DateHelper.js +77 -0
- package/dist/tools/DurationHelper.d.ts +6 -0
- package/dist/tools/DurationHelper.js +31 -0
- package/dist/tools/EnumHelper.d.ts +26 -0
- package/dist/tools/EnumHelper.js +40 -0
- package/dist/tools/ErrorHelper.d.ts +3 -0
- package/dist/tools/ErrorHelper.js +31 -0
- package/dist/tools/NumberHelper.d.ts +19 -0
- package/dist/tools/NumberHelper.js +49 -0
- package/dist/tools/ObjectHelper.d.ts +28 -0
- package/dist/tools/ObjectHelper.js +90 -0
- package/dist/tools/PromiseHelper.d.ts +3 -0
- package/dist/tools/PromiseHelper.js +7 -0
- package/dist/tools/StringHelper.d.ts +75 -0
- package/dist/tools/StringHelper.js +127 -0
- package/dist/tools/UidHelper.d.ts +56 -0
- package/dist/tools/UidHelper.js +132 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +9 -0
- package/dist/validation/Engine.d.ts +6 -0
- package/dist/validation/Engine.js +406 -0
- package/dist/validation/Result.d.ts +31 -0
- package/dist/validation/Result.js +264 -0
- package/dist/validation/Symbols.d.ts +1 -0
- package/dist/validation/Symbols.js +1 -0
- package/dist/validation/Types.d.ts +105 -0
- package/dist/validation/Types.js +1 -0
- package/dist/validation/Validator.d.ts +26 -0
- package/dist/validation/Validator.js +95 -0
- package/dist/validation/index.d.ts +5 -0
- package/dist/validation/index.js +5 -0
- package/dist/validation/messages/En.d.ts +3 -0
- package/dist/validation/messages/En.js +118 -0
- package/dist/validation/messages/Ru.d.ts +3 -0
- package/dist/validation/messages/Ru.js +118 -0
- package/dist/validation/messages/index.d.ts +7 -0
- package/dist/validation/messages/index.js +7 -0
- package/dist/validation/rules/CustomRules.d.ts +8 -0
- package/dist/validation/rules/CustomRules.js +64 -0
- package/dist/validation/rules/Factory.d.ts +4 -0
- package/dist/validation/rules/Factory.js +21 -0
- package/dist/validation/rules/StringRules.d.ts +105 -0
- package/dist/validation/rules/StringRules.js +481 -0
- package/dist/validation/rules/StructureRules.d.ts +30 -0
- package/dist/validation/rules/StructureRules.js +145 -0
- package/dist/validation/rules/ValueRules.d.ts +82 -0
- package/dist/validation/rules/ValueRules.js +365 -0
- package/dist/validation/rules/index.d.ts +151 -0
- package/dist/validation/rules/index.js +19 -0
- package/package.json +77 -3
package/VALIDATION.md
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
# Validation API
|
|
2
|
+
|
|
3
|
+
This reference describes the current `src/validation` implementation and its
|
|
4
|
+
runtime and TypeScript tests. Validation is framework-independent, synchronous
|
|
5
|
+
or asynchronous, and noncoercive. It checks caller-owned data; it is not a parser
|
|
6
|
+
that creates a transformed output.
|
|
7
|
+
|
|
8
|
+
## Imports and public exports
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import {
|
|
12
|
+
Validator,
|
|
13
|
+
createValidator,
|
|
14
|
+
createRules,
|
|
15
|
+
rules,
|
|
16
|
+
ValidationResult,
|
|
17
|
+
ValidationError,
|
|
18
|
+
validationMessagesEn,
|
|
19
|
+
validationMessagesRu,
|
|
20
|
+
validationMessagesI18n,
|
|
21
|
+
} from "@azure-net/node-kit/validation";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The package root is intentionally not exported. Catalog-only entry
|
|
25
|
+
points are `@azure-net/node-kit/validation/messages`,
|
|
26
|
+
`@azure-net/node-kit/validation/messages/en` (`validationMessagesEn`), and
|
|
27
|
+
`@azure-net/node-kit/validation/messages/ru` (`validationMessagesRu`).
|
|
28
|
+
|
|
29
|
+
Public types exported by the validation entry point:
|
|
30
|
+
|
|
31
|
+
| Group | Types |
|
|
32
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
33
|
+
| Rules | `RuleOptions`, `RuleFailure`, `RuleResult`, `RuleContext`, `SyncValidationRule`, `AsyncValidationRule`, `ValidationRule`, `BuiltinRule`, `FieldRule`, `ValidationRules`, `AnyRule`, `TypedRules` |
|
|
34
|
+
| Messages | `ValidationPath`, `ValidationParams`, `MessageContext`, `ValidationMessage`, `ValidationMessages`, `ValidationLocales` |
|
|
35
|
+
| Results/configuration | `ValidationIssue`, `ValidationErrors`, `ValidatorOptions`, `ValidatorCreateOptions`, `ValidationOptions`, `ValidationArguments` |
|
|
36
|
+
| String options | `LengthOptions`, `UuidOptions`, `UrlOptions`, `NanoidOptions`, `JwtOptions`, `MacOptions`, `HashOptions`, `HashAlgorithm`, `IsoTimeOptions`, `IsoDateTimeOptions` |
|
|
37
|
+
| Value options | `ValueRangeOptions`, `ValueClockOptions`, `ValueFileOptions` |
|
|
38
|
+
|
|
39
|
+
Use the merged `rules` object, not internal modules. There is no public
|
|
40
|
+
`/validation/rules` package subpath. `testRule`, `descriptor`, the engine functions,
|
|
41
|
+
and individual internal rule-group objects are not public entry-point exports.
|
|
42
|
+
`AnyRule` is the exported erased representation; prefer the typed callback types.
|
|
43
|
+
|
|
44
|
+
## Define a validator
|
|
45
|
+
|
|
46
|
+
Every field has an **array of rules**, not a fluent chain. Define a no-argument
|
|
47
|
+
subclass and call its static methods; do not instantiate it to validate input.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import {
|
|
51
|
+
Validator,
|
|
52
|
+
rules as r,
|
|
53
|
+
type ValidationRules,
|
|
54
|
+
} from "@azure-net/node-kit/validation";
|
|
55
|
+
|
|
56
|
+
interface Registration {
|
|
57
|
+
email: string;
|
|
58
|
+
password: string;
|
|
59
|
+
confirmation: string;
|
|
60
|
+
nickname?: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class RegistrationValidator extends Validator<Registration> {
|
|
64
|
+
protected rules(): ValidationRules<Registration> {
|
|
65
|
+
return {
|
|
66
|
+
email: [r.string(), r.email()],
|
|
67
|
+
password: [r.string(), r.minLength(12)],
|
|
68
|
+
confirmation: [r.string(), r.sameAs("password")],
|
|
69
|
+
nickname: [r.nullish(), r.string(), r.maxLength(40)],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const input: unknown = {
|
|
75
|
+
email: "ada@example.com",
|
|
76
|
+
password: "a-long-password",
|
|
77
|
+
confirmation: "a-long-password",
|
|
78
|
+
};
|
|
79
|
+
const data: Registration = RegistrationValidator.validateAndThrow(input);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`ValidationRules<T, TRoot = T, TContext = unknown>` requires entries for every
|
|
83
|
+
model key, including optional properties. Structural rule output types must fit
|
|
84
|
+
the declared field type. A TypeScript `?` does not add runtime optional behavior;
|
|
85
|
+
use `optional()` or `nullish()` explicitly. There is no schema-to-model inference
|
|
86
|
+
API replacing the declared `T`, and `unknown()` does not prove a narrower model.
|
|
87
|
+
|
|
88
|
+
Schema field keys must be strings or numbers, not symbols. `ValidationRules`
|
|
89
|
+
rejects symbol-keyed fields at compile time. At runtime, a symbol-keyed subclass
|
|
90
|
+
definition throws `TypeError` when its schema is first built; `object()` and both
|
|
91
|
+
`intersection()` shapes reject own symbol keys at construction. Numeric object
|
|
92
|
+
keys become string property paths. This restriction is on schema keys, not values:
|
|
93
|
+
`symbol()` still validates a symbol value in a supported field.
|
|
94
|
+
|
|
95
|
+
Each visited field must have a structural rule such as `string()`, `number()`,
|
|
96
|
+
`object()`, or `unknown()`. A list containing only `email()`, `minLength()`, a
|
|
97
|
+
callback, or no rules throws `TypeError` for a non-skipped value. Markers alone
|
|
98
|
+
do not guard non-nullish values. `optional()`, `nullable()`, and `nullish()` skip
|
|
99
|
+
all other rules for their accepted nullish values, regardless of array order.
|
|
100
|
+
Thus `[optional(), required(), string()]` still permits `undefined`.
|
|
101
|
+
|
|
102
|
+
## Four static methods
|
|
103
|
+
|
|
104
|
+
All methods accept `input: unknown`, not an already trusted `T`.
|
|
105
|
+
|
|
106
|
+
| Method | Return | Invalid data |
|
|
107
|
+
| ------------------------------------------ | ------------------------------ | --------------------------------- |
|
|
108
|
+
| `V.validate(input, options?)` | `ValidationResult<T>` | Failed result |
|
|
109
|
+
| `V.validateAndThrow(input, options?)` | `T` | Throws `ValidationError<T>` |
|
|
110
|
+
| `V.validateAsync(input, options?)` | `Promise<ValidationResult<T>>` | Resolves to failed result |
|
|
111
|
+
| `V.validateAndThrowAsync(input, options?)` | `Promise<T>` | Rejects with `ValidationError<T>` |
|
|
112
|
+
|
|
113
|
+
`ValidationOptions<C>` contains `locale?: string`, `context?: C`, and
|
|
114
|
+
`signal?: AbortSignal`. With a concrete dependency type `C` on `Validator<T, C>`,
|
|
115
|
+
the call requires an options object with `context: C`; locale alone is not enough.
|
|
116
|
+
With the default `unknown` context, options are optional. Context is typed, not
|
|
117
|
+
runtime-validated by the library.
|
|
118
|
+
|
|
119
|
+
`result.isValid` is a boolean, not a discriminated-union type guard:
|
|
120
|
+
`result.data` remains `T | undefined` in TypeScript even after checking it.
|
|
121
|
+
The throwing APIs provide a direct `T` on success.
|
|
122
|
+
|
|
123
|
+
Unexpected exceptions from callbacks, message functions, locale providers, or
|
|
124
|
+
dependencies propagate unchanged. They are not validation issues. Cancellation
|
|
125
|
+
propagates the signal's abort reason, not `ValidationError`.
|
|
126
|
+
|
|
127
|
+
## Configured base classes
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import {
|
|
131
|
+
Validator,
|
|
132
|
+
validationMessagesRu,
|
|
133
|
+
validationMessagesI18n,
|
|
134
|
+
} from "@azure-net/node-kit/validation";
|
|
135
|
+
|
|
136
|
+
const RussianValidator = new Validator.create({
|
|
137
|
+
messages: validationMessagesRu,
|
|
138
|
+
defaultLocale: "ru",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Supply this function from application-owned request storage.
|
|
142
|
+
declare function currentRequestLocale(): string | undefined;
|
|
143
|
+
const LocalizedValidator = new Validator.create({
|
|
144
|
+
messages: validationMessagesI18n,
|
|
145
|
+
defaultLocale: "en",
|
|
146
|
+
fallbackLocale: "en",
|
|
147
|
+
getLocale: () => currentRequestLocale(),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Extend RussianValidator<Model> or LocalizedValidator<Model, Dependencies>.
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`Validator.create(options)`, `new Validator.create(options)`, and
|
|
154
|
+
`createValidator(options)` all return a configured **base class**, not a validator
|
|
155
|
+
instance or a schema. Configuration and copied catalogs are frozen. A flat
|
|
156
|
+
`messages` catalog applies to every locale. A locale-to-catalog `messages` map
|
|
157
|
+
selects translations by locale. Do not mix flat entries and nested catalogs.
|
|
158
|
+
Explicit `locales` takes precedence over locale-keyed `messages` when both are
|
|
159
|
+
supplied. The class exposes `static readonly configuration: ValidatorOptions`.
|
|
160
|
+
|
|
161
|
+
| Option | Default | Meaning |
|
|
162
|
+
| ---------------- | ---------------------- | ---------------------------------------------------------------------------------------------- |
|
|
163
|
+
| `messages` | None | Flat overrides, or locale-keyed catalogs in `ValidatorCreateOptions` |
|
|
164
|
+
| `locales` | Built-in `en`/`ru` map | Explicit locale-keyed catalogs |
|
|
165
|
+
| `defaultLocale` | `"en"` | Locale when neither call nor provider supplies one |
|
|
166
|
+
| `fallbackLocale` | `"en"` | Fallback catalog lookup |
|
|
167
|
+
| `getLocale` | None | Synchronous `() => string \| undefined`, evaluated per validation |
|
|
168
|
+
| `unknownKeys` | `"reject"` | Object unknown-key policy; `"allow"` preserves keys |
|
|
169
|
+
| `cascade` | `"stop"` | Stop after first failed check per field; `"continue"` collects more built-in constraint issues |
|
|
170
|
+
| `maxNodes` | `10000` | Traversal budget; configured integer range `1..10000` |
|
|
171
|
+
| `maxDepth` | `64` | Traversal depth; configured integer range `1..64` |
|
|
172
|
+
| `maxIssues` | `100` | Diagnostic count; configured integer range `1..10000` |
|
|
173
|
+
|
|
174
|
+
Factories reject invalid limits with `RangeError`. Root depth starts at zero and
|
|
175
|
+
counts as a node. Nested values, keys, lazy/validator expansions, and union
|
|
176
|
+
alternatives consume traversal work; union branches share the node budget.
|
|
177
|
+
Depth/node exhaustion is invalid (`validation.depth` / `validation.nodes`, or a
|
|
178
|
+
containing union failure). Reaching `maxIssues` truncates diagnostics without an
|
|
179
|
+
extra truncation issue. These are traversal/diagnostic limits, not input-byte,
|
|
180
|
+
CPU-time, regex, network-timeout, or custom-callback allocation limits.
|
|
181
|
+
|
|
182
|
+
## Execution, ownership, and safety
|
|
183
|
+
|
|
184
|
+
1. The engine prepares structure across the input before user callbacks. Nullish
|
|
185
|
+
markers skip their accepted values first. For unskipped null/undefined values,
|
|
186
|
+
explicit `required()`, `notNull()`, and `notEmpty()` predicates run before type
|
|
187
|
+
guards so their presence messages are retained. Otherwise, structural guards
|
|
188
|
+
run before value constraints irrespective of their positions in a list.
|
|
189
|
+
2. A structural failure in the main traversal suppresses **all** user callbacks,
|
|
190
|
+
including callbacks on otherwise valid siblings. Rejected union alternatives
|
|
191
|
+
are isolated; a valid alternative can still succeed.
|
|
192
|
+
3. Built-in constraints run before callbacks on a field. Child jobs precede parent
|
|
193
|
+
callbacks. A failed field or descendant suppresses that field's callbacks.
|
|
194
|
+
4. The default cascade stops on the first failed check per field. `"continue"`
|
|
195
|
+
allows further built-in constraints, but does not make callbacks run after a
|
|
196
|
+
failure. An unrelated value-constraint failure need not suppress valid siblings.
|
|
197
|
+
5. Async rules and attempted union branches are awaited sequentially, not in
|
|
198
|
+
parallel. An ordinary `union()` discards failed branch diagnostics on success;
|
|
199
|
+
failure reports a single union issue rather than a public branch-error tree.
|
|
200
|
+
`discriminatedUnion()` instead validates its selected branch directly and
|
|
201
|
+
retains precise nested issue paths and messages from that branch.
|
|
202
|
+
|
|
203
|
+
The nullish presence precheck stops at the first failed presence predicate in
|
|
204
|
+
rule-array order, even with `cascade: "continue"`. That failure marks preparation
|
|
205
|
+
as structurally invalid and suppresses user callbacks across the validation.
|
|
206
|
+
If all applicable presence checks pass, normal type checking proceeds;
|
|
207
|
+
`notNull()` alone accepts undefined, so a following `string()` can still reject it.
|
|
208
|
+
Presence predicates do not replace the requirement for a structural rule.
|
|
209
|
+
|
|
210
|
+
For example, `[string(), required({ message: "Name is required" })]` reports
|
|
211
|
+
`"Name is required"` for null or a missing value, but a `string` type error for
|
|
212
|
+
the non-nullish value `42`. Empty strings are non-nullish: they undergo normal
|
|
213
|
+
type checking first, then constraints such as `notEmpty()`. An accepting
|
|
214
|
+
`optional()`, `nullable()`, or `nullish()` marker still bypasses presence checks.
|
|
215
|
+
|
|
216
|
+
Sync methods throw `TypeError` if an executed rule returns a promise or thenable.
|
|
217
|
+
Use async methods for schemas that may perform async work. Detecting a promise
|
|
218
|
+
cannot undo a callback or side effect that already started. The sync engine
|
|
219
|
+
observes rejection to avoid an unhandled rejection, but cannot cancel that work.
|
|
220
|
+
Similarly, later union attempts cannot undo earlier attempts' side effects.
|
|
221
|
+
|
|
222
|
+
Signals are checked before traversal/jobs and after awaited results. Forward
|
|
223
|
+
`signal` to dependencies for cooperative cancellation; validation does not race
|
|
224
|
+
or forcibly interrupt a pending promise, synchronous computation, or external I/O.
|
|
225
|
+
|
|
226
|
+
The library does not mutate, trim, normalize, coerce, strip, default, or clone
|
|
227
|
+
validated input. Successful `data` is the **same reference** as input. Caller-owned
|
|
228
|
+
objects remain unfrozen and mutable, so neither callers nor callbacks should
|
|
229
|
+
mutate input in-flight, especially across awaits. Diagnostics are snapshots, not
|
|
230
|
+
the successful data. Copy data yourself before validation if isolation is needed.
|
|
231
|
+
|
|
232
|
+
Object fields and array indices are read through own property descriptors.
|
|
233
|
+
Accessors encountered during structural traversal are rejected with
|
|
234
|
+
`object.accessor` without invoking their getters/setters. Inherited values cannot
|
|
235
|
+
satisfy declared fields; sparse array holes are treated as `undefined`.
|
|
236
|
+
Unknown own enumerable string keys reject with `object.unknownKeys` by default;
|
|
237
|
+
`object(shape, { unknownKeys: "allow" })` overrides the global policy locally.
|
|
238
|
+
Allowed keys are not stripped or recursively checked. Symbol and non-enumerable
|
|
239
|
+
unknown keys are not part of this enumeration. `record()` validates own enumerable
|
|
240
|
+
string keys. Object validation is not restricted to plain-object prototypes, but
|
|
241
|
+
rejects arrays, Dates, Maps, Sets, RegExps, ArrayBuffers, and buffer views.
|
|
242
|
+
|
|
243
|
+
These protections are not a sandbox for arbitrary executable objects: `unknown()`
|
|
244
|
+
does not traverse contents, custom callbacks can access anything, proxies can
|
|
245
|
+
execute traps, and rules such as `uniqueBy()` read selected properties. Use
|
|
246
|
+
ordinary data objects from a trusted parsing boundary. Traversed cycles reject;
|
|
247
|
+
shared acyclic references are allowed.
|
|
248
|
+
|
|
249
|
+
`rules()` is called once per subclass, on a new no-argument instance, and its
|
|
250
|
+
shape/rule arrays are cached. Lazy resolvers and child definitions are also cached.
|
|
251
|
+
Do not capture a request, tenant, current user, locale, or request-specific service
|
|
252
|
+
inside `rules()` or a lazy resolver. Read per-call dependencies from `context`,
|
|
253
|
+
per-call data from `root`/`data`, and locale through `getLocale` or call options.
|
|
254
|
+
|
|
255
|
+
## Custom rules and typed dependencies
|
|
256
|
+
|
|
257
|
+
Public callback signatures (all imported from the validation entry point):
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
type SyncValidationRule<
|
|
261
|
+
TValue,
|
|
262
|
+
TData = unknown,
|
|
263
|
+
TRoot = TData,
|
|
264
|
+
TContext = unknown,
|
|
265
|
+
> = (context: RuleContext<TValue, TData, TRoot, TContext>) => RuleResult;
|
|
266
|
+
type AsyncValidationRule<
|
|
267
|
+
TValue,
|
|
268
|
+
TData = unknown,
|
|
269
|
+
TRoot = TData,
|
|
270
|
+
TContext = unknown,
|
|
271
|
+
> = (
|
|
272
|
+
context: RuleContext<TValue, TData, TRoot, TContext>,
|
|
273
|
+
) => PromiseLike<RuleResult>;
|
|
274
|
+
type ValidationRule<
|
|
275
|
+
TValue,
|
|
276
|
+
TData = unknown,
|
|
277
|
+
TRoot = TData,
|
|
278
|
+
TContext = unknown,
|
|
279
|
+
> = (
|
|
280
|
+
context: RuleContext<TValue, TData, TRoot, TContext>,
|
|
281
|
+
) => RuleResult | PromiseLike<RuleResult>;
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`RuleContext` has readonly `value`, `data` (immediate parent object/collection),
|
|
285
|
+
`root` (current validator root), `context` (dependencies), `path` (absolute
|
|
286
|
+
`readonly (string | number)[]`), and `signal: AbortSignal | undefined`.
|
|
287
|
+
Readonly context properties do not deep-freeze their referenced objects.
|
|
288
|
+
|
|
289
|
+
`RuleResult` is `boolean | RuleFailure | readonly RuleFailure[]`:
|
|
290
|
+
|
|
291
|
+
| Return | Meaning |
|
|
292
|
+
| ---------------------------------------------------- | ---------------------------------- |
|
|
293
|
+
| `true` or `[]` | Success |
|
|
294
|
+
| `false` | Failure with code `custom.invalid` |
|
|
295
|
+
| `{ message, code, messageKey, label, params, path }` | One failure; all members optional |
|
|
296
|
+
| Array of failure objects | Multiple issues |
|
|
297
|
+
|
|
298
|
+
`RuleFailure` extends `RuleOptions` with `params?: Readonly<Record<string, unknown>>`
|
|
299
|
+
and `path?: ValidationPath`. Failure paths are **relative to the current field**,
|
|
300
|
+
not absolute. Omit the path for the field itself. Return a failure object for an
|
|
301
|
+
expected rejection; throw for an operational failure. Strings, `undefined`, and
|
|
302
|
+
arrays of booleans are not valid results.
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import {
|
|
306
|
+
Validator,
|
|
307
|
+
rules as r,
|
|
308
|
+
type AsyncValidationRule,
|
|
309
|
+
type ValidationRules,
|
|
310
|
+
} from "@azure-net/node-kit/validation";
|
|
311
|
+
|
|
312
|
+
interface Account {
|
|
313
|
+
email: string;
|
|
314
|
+
}
|
|
315
|
+
interface Dependencies {
|
|
316
|
+
emailExists(email: string, signal?: AbortSignal): Promise<boolean>;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export const unusedEmail: AsyncValidationRule<
|
|
320
|
+
string,
|
|
321
|
+
Account,
|
|
322
|
+
Account,
|
|
323
|
+
Dependencies
|
|
324
|
+
> = async ({ value, context, signal }) =>
|
|
325
|
+
(await context.emailExists(value, signal))
|
|
326
|
+
? { code: "email.taken", message: "Email is already registered." }
|
|
327
|
+
: true;
|
|
328
|
+
|
|
329
|
+
class AccountValidator extends Validator<Account, Dependencies> {
|
|
330
|
+
protected rules(): ValidationRules<Account, Account, Dependencies> {
|
|
331
|
+
return { email: [r.string(), r.email(), unusedEmail] };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
declare const input: unknown;
|
|
336
|
+
declare const dependencies: Dependencies;
|
|
337
|
+
const account: Account = await AccountValidator.validateAndThrowAsync(input, {
|
|
338
|
+
context: dependencies,
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Typed nested rules
|
|
343
|
+
|
|
344
|
+
`createRules<T, C>()` returns `TypedRules<T, C>`, a frozen facade over `rules`.
|
|
345
|
+
Its `object`, `array`, `record`, `map`, `set`, and `tuple` helpers preserve the root
|
|
346
|
+
`T` and dependency `C` types inside nested callbacks, while `data` is the immediate
|
|
347
|
+
container. The bound `object()` inference overload uses the leading built-in
|
|
348
|
+
descriptor in each field's rule array to infer the field type, then contextually
|
|
349
|
+
types subsequent callbacks without letting them widen that inferred model.
|
|
350
|
+
For example, `[r.string(), callback]` infers a string value and the corresponding
|
|
351
|
+
typed parent object. A second overload accepts `ValidationRules<V, T, C>`;
|
|
352
|
+
explicit nested generics remain available for pretyped shapes or when inference
|
|
353
|
+
is insufficient. Leading descriptors matter for this inference overload, not
|
|
354
|
+
for changing runtime rule execution order.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import {
|
|
358
|
+
createRules,
|
|
359
|
+
type ValidationRules,
|
|
360
|
+
} from "@azure-net/node-kit/validation";
|
|
361
|
+
|
|
362
|
+
interface Order {
|
|
363
|
+
tenant: string;
|
|
364
|
+
rows: { sku: string }[];
|
|
365
|
+
}
|
|
366
|
+
interface Services {
|
|
367
|
+
accepts(tenant: string, sku: string): boolean;
|
|
368
|
+
}
|
|
369
|
+
const r = createRules<Order, Services>();
|
|
370
|
+
|
|
371
|
+
const orderRules: ValidationRules<Order, Order, Services> = {
|
|
372
|
+
tenant: [r.string()],
|
|
373
|
+
rows: [
|
|
374
|
+
r.array<Order["rows"][number]>([
|
|
375
|
+
r.object({
|
|
376
|
+
sku: [
|
|
377
|
+
r.string(),
|
|
378
|
+
({ value, data, root, context }) =>
|
|
379
|
+
data.sku === value && context.accepts(root.tenant, value),
|
|
380
|
+
],
|
|
381
|
+
}),
|
|
382
|
+
]),
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
For raw helpers, supply their root/context generics explicitly if needed, for
|
|
388
|
+
example `rules.object<Child, Root, Dependencies>(shape)`. Unbound nested helpers
|
|
389
|
+
default unknown root/context types. The facade does not rebind `union`,
|
|
390
|
+
`discriminatedUnion`, `intersection`, `lazy`, or `when`; those retain their own
|
|
391
|
+
generic signatures.
|
|
392
|
+
|
|
393
|
+
`createRules<T, C>().sameAs(key)` and `.notSameAs(key)` restrict keys to string keys
|
|
394
|
+
of `T`. They still compare the **immediate sibling** at runtime, never the root or
|
|
395
|
+
a dotted path. For nested sibling names, use `createRules<Child>()` for comparison
|
|
396
|
+
descriptors or raw `rules.sameAs("childKey")`; a root-bound facade does not
|
|
397
|
+
automatically change its allowed comparison keys when entering a nested object.
|
|
398
|
+
|
|
399
|
+
### Child-validator boundary
|
|
400
|
+
|
|
401
|
+
`rules.validator(ChildValidator)` reuses the child's cached schema. Inside it,
|
|
402
|
+
`root` is the child value, even under arrays or multiple nested validator boundaries.
|
|
403
|
+
Paths remain absolute in the outer input; dependency context and signal remain
|
|
404
|
+
the outer call's. A callback beside `validator(...)` in the outer field's array
|
|
405
|
+
still sees the outer root. Ordinary `object()` and `lazy()` do not reset root.
|
|
406
|
+
|
|
407
|
+
Child composition reuses the definition, not the child's static validation method.
|
|
408
|
+
The child's configured `unknownKeys` policy applies throughout its subtree,
|
|
409
|
+
defaulting to `"reject"` even when the parent allows unknown keys. Thus a child
|
|
410
|
+
configured with `"allow"` permits unknown keys within its subtree even under a
|
|
411
|
+
rejecting parent. Explicit nested `object(..., { unknownKeys })` options override
|
|
412
|
+
the subtree policy locally; a further `validator(...)` boundary establishes that
|
|
413
|
+
child's own policy. Siblings outside the child subtree retain the parent's policy.
|
|
414
|
+
|
|
415
|
+
All other configuration, including localization, cascade, and shared traversal/
|
|
416
|
+
issue budgets, remains inherited from the outer validation call; child defaults
|
|
417
|
+
for those settings are not independently applied. Descriptors now carry dependency
|
|
418
|
+
and root types as `BuiltinRule<TValue, TContext, TRoot>`. Child descriptors preserve
|
|
419
|
+
their dependency contract, so typed parent rule maps must supply a compatible
|
|
420
|
+
context, including through structural wrappers. Dependencies are still supplied
|
|
421
|
+
by the caller and are not runtime-validated.
|
|
422
|
+
|
|
423
|
+
## Messages and locale resolution
|
|
424
|
+
|
|
425
|
+
`RuleOptions` is `{ message?, messageKey?, code?, label? }`. Messages are strings
|
|
426
|
+
or `(context: MessageContext) => string`. `MessageContext` exposes `code`,
|
|
427
|
+
`messageKey`, `params`, `path`, `locale`, and `label`.
|
|
428
|
+
|
|
429
|
+
Locale precedence, resolved once per validation:
|
|
430
|
+
|
|
431
|
+
1. Call option `locale`.
|
|
432
|
+
2. Configured `getLocale()` result.
|
|
433
|
+
3. Configured `defaultLocale`.
|
|
434
|
+
4. `"en"`.
|
|
435
|
+
|
|
436
|
+
An explicit locale bypasses `getLocale`. Locale lookup uses exact keys; there is
|
|
437
|
+
no language negotiation or automatic `ru-RU` to `ru` conversion. The application
|
|
438
|
+
should resolve supported language tags before calling validation.
|
|
439
|
+
|
|
440
|
+
For each issue, lookup uses `messageKey ?? originalCode`, in this order:
|
|
441
|
+
|
|
442
|
+
1. Rule/failure `message`.
|
|
443
|
+
2. Flat configured `messages[key]`.
|
|
444
|
+
3. Configured selected-locale catalog (or built-in locale map when none configured).
|
|
445
|
+
4. Built-in selected-locale catalog.
|
|
446
|
+
5. Configured fallback-locale catalog (or built-in fallback map when none configured).
|
|
447
|
+
6. Built-in English catalog.
|
|
448
|
+
7. Literal `"Invalid value"`.
|
|
449
|
+
|
|
450
|
+
The requested locale remains in `MessageContext.locale` even when fallback text
|
|
451
|
+
is used. Built-in catalogs currently cover `en` and `ru`, not arbitrary languages.
|
|
452
|
+
Do not assume every possible descriptor code has a translation: for example,
|
|
453
|
+
current exact-tuple-length failures use `array.length`, and absent entries follow
|
|
454
|
+
the fallback chain. Add application catalog entries for custom codes.
|
|
455
|
+
|
|
456
|
+
For a built-in rule, `code` overrides the emitted issue code without changing its
|
|
457
|
+
default lookup key; use `messageKey` to select a different translation. A custom
|
|
458
|
+
failure's `code` is its base lookup code unless `messageKey` is supplied. Failure
|
|
459
|
+
options override descriptor options; failure params merge over descriptor params.
|
|
460
|
+
|
|
461
|
+
After a string or function message is obtained, `{label}` uses the explicit label
|
|
462
|
+
or the last path segment (`"value"` at root). Other `{word}` placeholders use own
|
|
463
|
+
params and `String(...)`; missing placeholders remain unchanged. This is simple
|
|
464
|
+
interpolation, not ICU formatting, escaping, pluralization, or HTML sanitization.
|
|
465
|
+
Never include secrets in messages or params.
|
|
466
|
+
|
|
467
|
+
## Results and HTTP errors
|
|
468
|
+
|
|
469
|
+
`ValidationResult<T>` exposes readonly `isValid`, `data`, and `issues`.
|
|
470
|
+
Each `ValidationIssue` contains `path`, `code`, rendered `message`, and `params`.
|
|
471
|
+
Failed results discard input (`data === undefined`); successful results retain it
|
|
472
|
+
by reference. The public constructor is `new ValidationResult(data, issues)`;
|
|
473
|
+
no issues means success, even if the supplied data is `undefined`.
|
|
474
|
+
|
|
475
|
+
`errors: ValidationErrors<T>` is a lazily built, cached, frozen tree:
|
|
476
|
+
|
|
477
|
+
- `messages` stores issues at that exact node, including root/form errors.
|
|
478
|
+
- `fields` stores string path segments, including literal dots and brackets.
|
|
479
|
+
- `items` stores numeric path segments, filling preceding unaffected indices with
|
|
480
|
+
explicit `null` placeholders, not sparse holes. It is not padded to input length.
|
|
481
|
+
- Parent messages are retained alongside child `fields`/`items`; they are never
|
|
482
|
+
replaced by descendant errors. This does not imply every parent rule executes
|
|
483
|
+
after a child has failed.
|
|
484
|
+
|
|
485
|
+
For issues at root, `rows`, and `rows[2].name`, a possible serialized tree is:
|
|
486
|
+
|
|
487
|
+
```json
|
|
488
|
+
{
|
|
489
|
+
"messages": ["Form is invalid"],
|
|
490
|
+
"fields": {
|
|
491
|
+
"rows": {
|
|
492
|
+
"messages": ["Rows need review"],
|
|
493
|
+
"items": [
|
|
494
|
+
null,
|
|
495
|
+
null,
|
|
496
|
+
{
|
|
497
|
+
"messages": [],
|
|
498
|
+
"fields": { "name": { "messages": ["Name is required"] } }
|
|
499
|
+
}
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Separate namespaces allow input fields named `messages`, `fields`, `items`, or
|
|
507
|
+
`__proto__`. String `"0"` is a field; numeric `0` is an item.
|
|
508
|
+
`formErrors` returns root messages. `toFieldErrors()` returns a frozen flat record
|
|
509
|
+
of RFC 6901 JSON Pointer keys to message arrays: `~` becomes `~0`, `/` becomes
|
|
510
|
+
`~1`, and root errors are omitted. Numeric and string zero share a flat pointer;
|
|
511
|
+
use the tree when that distinction matters.
|
|
512
|
+
|
|
513
|
+
Issues, paths, and metadata are detached frozen snapshots. Metadata is converted
|
|
514
|
+
to JSON-safe data: bigints become decimal strings, valid Dates ISO strings,
|
|
515
|
+
RegExps `/source/flags`, invalid Dates/nonfinite numbers `null`, and cycles
|
|
516
|
+
`"[Circular]"`. Accessors, functions, symbols, undefined, and proxies are omitted
|
|
517
|
+
(or `null` in arrays); getters and `toJSON` are not invoked during snapshotting.
|
|
518
|
+
Snapshots are bounded: depth 32, container width 1000, per-result 10000 entries
|
|
519
|
+
and 100000 characters, individual text up to 4096 characters. Truncation uses
|
|
520
|
+
`"[Truncated]"` and sometimes a `$snapshot` marker; oversized keys are omitted.
|
|
521
|
+
These limits do not sanitize rendered message text.
|
|
522
|
+
|
|
523
|
+
Public result construction also caps issues at 10000, path depth at 128, numeric
|
|
524
|
+
indices at `0..10000`, tree nodes at 100000, and aggregate array slots at 100000.
|
|
525
|
+
Invalid paths or excessive allocations throw rather than producing an unsafe
|
|
526
|
+
tree. Custom relative paths are subject to these bounds too.
|
|
527
|
+
|
|
528
|
+
`ValidationError<T>` extends `Error`, requires a failed result, and exposes
|
|
529
|
+
`result`, `issues`, and `errors`. Its default message is `"Validation failed"`;
|
|
530
|
+
`new ValidationError(result, message)` overrides it. `toJSON()` includes only
|
|
531
|
+
`name`, `message`, `issues`, and `errors`, not input or stack. Application-supplied
|
|
532
|
+
diagnostic text/params can still expose data, so avoid sensitive values.
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
import { ValidationError } from "@azure-net/node-kit/validation";
|
|
536
|
+
import { ResponseBuilder } from "@azure-net/node-kit/delivery/http";
|
|
537
|
+
|
|
538
|
+
function validationResponse(error: unknown): Response {
|
|
539
|
+
if (!(error instanceof ValidationError)) throw error;
|
|
540
|
+
return ResponseBuilder.UnprocessableEntity(null, {
|
|
541
|
+
message: "Validation failed",
|
|
542
|
+
code: "VALIDATION_FAILED",
|
|
543
|
+
errors: error.errors,
|
|
544
|
+
}).toResponse();
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
There is no automatic HTTP mapping. `UnprocessableEntity(data?, options?)` fixes
|
|
549
|
+
status 422; without a code override its code is `UNPROCESSABLE_ENTITY`. Pass
|
|
550
|
+
`error.issues` instead if the client needs codes, params, and paths. The response's
|
|
551
|
+
top-level message is independent of localized field messages.
|
|
552
|
+
|
|
553
|
+
### Hono integration (conceptual)
|
|
554
|
+
|
|
555
|
+
Hono is **not a dependency** of this package. In a Hono application, install the
|
|
556
|
+
application's context-storage middleware before routes, then provide a
|
|
557
|
+
`getLocale` closure that reads the current request's resolved locale from that
|
|
558
|
+
storage. Read storage when validation starts, not while constructing the schema.
|
|
559
|
+
Use the application's supported-locale selection policy; do not treat the raw
|
|
560
|
+
`Accept-Language` header as a catalog key.
|
|
561
|
+
|
|
562
|
+
At a route boundary, parse the body into `unknown`, pass explicit typed services
|
|
563
|
+
as `context`, and await `validateAndThrowAsync`. Catch `ValidationError` in the
|
|
564
|
+
application error boundary and build a 422 envelope as above; operational errors
|
|
565
|
+
must follow a different error path. A Hono handler can return the native
|
|
566
|
+
`response.toResponse()` or `c.json(response.toJSON(), response.status)`. `toJSON()`
|
|
567
|
+
returns an object; do not JSON-stringify it before passing it to `c.json`.
|
|
568
|
+
Alternatively pass `locale` explicitly and bypass context storage altogether.
|
|
569
|
+
The toolkit creates no Hono adapter, global locale, or request storage itself.
|
|
570
|
+
|
|
571
|
+
## Complete built-in rule catalog
|
|
572
|
+
|
|
573
|
+
Below, `o?` means optional `RuleOptions` unless a more specific exported option
|
|
574
|
+
type is stated. Factories return immutable descriptors except `when`, `some`, and
|
|
575
|
+
`every`, which return callbacks. Constraints need an accompanying structural
|
|
576
|
+
guard even if they independently reject wrong types. Invalid factory arguments
|
|
577
|
+
generally throw `TypeError` or `RangeError`, not a failed validation result.
|
|
578
|
+
|
|
579
|
+
### Structural scalars and presence
|
|
580
|
+
|
|
581
|
+
| Rule | Semantics |
|
|
582
|
+
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
583
|
+
| `string(o?)` | Primitive strings, including empty strings; structural guard |
|
|
584
|
+
| `number(o?)`, `finite(o?)` | Finite primitive numbers, excluding NaN/infinities; structural guards |
|
|
585
|
+
| `bigint(o?)`, `boolean(o?)`, `symbol(o?)` | Exact primitive type; structural guards |
|
|
586
|
+
| `date(o?)` | Valid `Date` instance, not a timestamp/string; structural guard |
|
|
587
|
+
| `null(o?)`, `undefined(o?)`, `nan(o?)` | Exactly null, undefined, or numeric NaN; structural guards |
|
|
588
|
+
| `unknown(o?)` | Accept any value without traversing it; structural guard |
|
|
589
|
+
| `never(o?)` | Reject every value; structural guard |
|
|
590
|
+
| `instanceOf(constructor, o?)` | `instanceof` check against the supplied constructor; structural guard |
|
|
591
|
+
| `literal(expected, o?)` | `Object.is` equality to string, finite number, bigint, boolean, or null; structural guard |
|
|
592
|
+
| `enum(values, o?)` | Nonempty literal array, or string/number enum object; exact `Object.is` membership; structural guard |
|
|
593
|
+
| `enumName(enumObject, o?)` | String member names, excluding numeric reverse mappings; structural guard |
|
|
594
|
+
| `optional()` | Skip undefined/missing values |
|
|
595
|
+
| `nullable()` | Skip null values only |
|
|
596
|
+
| `nullish()` | Skip undefined/missing and null values |
|
|
597
|
+
| `required(o?)` | Reject null and undefined; accepts empty string, zero, and false; constraint |
|
|
598
|
+
| `notNull(o?)` | Reject null only (undefined passes); constraint |
|
|
599
|
+
| `empty(o?)`, `notEmpty(o?)` | Empty/nonempty constraint, not a type guard |
|
|
600
|
+
| `equal(expected, o?)`, `notEqual(expected, o?)` | Deep strict equality/inequality via Node `isDeepStrictEqual` |
|
|
601
|
+
|
|
602
|
+
Enum objects exclude TypeScript numeric reverse mappings. Arrays can include
|
|
603
|
+
bigint, boolean, and null; enum objects accept string/finite-number values only.
|
|
604
|
+
No conversion of enum names or numeric strings occurs. Erased `const enum`s
|
|
605
|
+
cannot supply a runtime object. `literal` distinguishes `-0` and `0`.
|
|
606
|
+
|
|
607
|
+
Empty means nullish, a string whose `trim()` is empty, an empty array/Map/Set/Blob,
|
|
608
|
+
or a plain/null-prototype object with no own keys (including symbol keys).
|
|
609
|
+
Whitespace is examined, not removed. Zero, false, `[undefined]`, and
|
|
610
|
+
`{ value: undefined }` are nonempty.
|
|
611
|
+
|
|
612
|
+
### Numbers, ranges, and decimals
|
|
613
|
+
|
|
614
|
+
| Rule | Semantics |
|
|
615
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
616
|
+
| `int32(o?)` | Structural number guard, integer `-2147483648..2147483647` |
|
|
617
|
+
| `uint32(o?)` | Structural number guard, integer `0..4294967295` |
|
|
618
|
+
| `float32(o?)` | Structural finite-number guard, absolute value at most `3.4028234663852886e38`; range only, no rounding |
|
|
619
|
+
| `int64(o?)` | Structural bigint guard, `-(2n ** 63n)..2n ** 63n - 1n` |
|
|
620
|
+
| `uint64(o?)` | Structural bigint guard, `0n..2n ** 64n - 1n` |
|
|
621
|
+
| `integer(o?)` | Integer finite number or any bigint; constraint |
|
|
622
|
+
| `safeInteger(o?)` | Number/bigint within JS safe-integer bounds; constraint |
|
|
623
|
+
| `positive(o?)`, `negative(o?)` | Finite number/bigint strictly above/below zero |
|
|
624
|
+
| `nonNegative(o?)`, `nonPositive(o?)` | Finite number/bigint including zero |
|
|
625
|
+
| `min(bound, o?)`, `max(bound, o?)` | Inclusive by default; `ValueRangeOptions.inclusive: false` excludes bound |
|
|
626
|
+
| `lessThan(bound, o?)`, `greaterThan(bound, o?)` | Exclusive by default; `inclusive: true` includes bound |
|
|
627
|
+
| `lessThanOrEqual(bound, o?)`, `greaterThanOrEqual(bound, o?)` | Always inclusive; ordinary `RuleOptions` |
|
|
628
|
+
| `between(min, max, o?)` | Same-type ordered bounds; both inclusive by default |
|
|
629
|
+
| `multipleOf(divisor, o?)` | Same-type finite number/bigint, nonzero divisor; exact decimal-number divisibility or bigint remainder |
|
|
630
|
+
| `decimal(places?, o?)` | Finite number or decimal-notation string, optionally bounded fractional places |
|
|
631
|
+
| `precisionScale(precision, scale, o?)` | At most `precision - scale` integer and `scale` fractional digits |
|
|
632
|
+
|
|
633
|
+
Bounds accept finite numbers, bigints, or valid Dates, but input must have the
|
|
634
|
+
same kind as the bound. Date targets are snapshotted as timestamps. Reversed or
|
|
635
|
+
mixed-kind `between` bounds throw. `between` endpoint settings
|
|
636
|
+
`minInclusive`/`maxInclusive` override `inclusive`; these endpoint settings do not
|
|
637
|
+
change single-bound checks.
|
|
638
|
+
|
|
639
|
+
`float32` is not an exact `Math.fround` representability test: it accepts `0.1`
|
|
640
|
+
and tiny finite doubles within range. 64-bit integer rules require bigint, not
|
|
641
|
+
JS numbers. `multipleOf(0.1)` accepts `0.3` but rejects `0.1 + 0.2`; it compares
|
|
642
|
+
shortest round-trip decimal number representations without epsilon tolerance.
|
|
643
|
+
|
|
644
|
+
Decimal checks accept signed decimal/exponent notation, including `.5` and `1.`,
|
|
645
|
+
but no whitespace, hex, separators, bigint, NaN, or infinity. Strings retain exact
|
|
646
|
+
digits; number inputs use their decimal representation. Trailing fractional zeros
|
|
647
|
+
do not consume scale. Input notation is limited to 4096 characters, and exponent
|
|
648
|
+
handling avoids exponent-sized allocations. `places`, precision, and scale are
|
|
649
|
+
nonnegative safe integers; precision must be positive and scale cannot exceed it.
|
|
650
|
+
These checks never convert the original string into a number. Pair with `string()`
|
|
651
|
+
for decimal strings or an explicit number/string union to accept both.
|
|
652
|
+
|
|
653
|
+
### Dates and files
|
|
654
|
+
|
|
655
|
+
| Rule | Semantics |
|
|
656
|
+
| ----------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
657
|
+
| `before(target, o?)`, `after(target, o?)` | Strict comparison to a valid Date |
|
|
658
|
+
| `same(target, o?)` | Same Date instant in milliseconds, not calendar-day equality |
|
|
659
|
+
| `isPast(o?)`, `isFuture(o?)` | Strict comparison to a per-check clock; `ValueClockOptions` |
|
|
660
|
+
| `file(o?)` | Structural real-File metadata validation; returns `BuiltinRule<File>`; `ValueFileOptions` |
|
|
661
|
+
| `blob(o?)` | Structural Blob/File metadata validation; returns `BuiltinRule<Blob>`; same `ValueFileOptions` |
|
|
662
|
+
|
|
663
|
+
Date constraints require Date input; pair with `date()`. Relative-date options
|
|
664
|
+
accept `clock?: () => Date | number`; default reads `Date.now()` for each check.
|
|
665
|
+
An invalid clock result throws. Target Dates are snapshotted at rule construction.
|
|
666
|
+
|
|
667
|
+
`file()` requires a genuine File instance, rejecting plain Blobs even if they
|
|
668
|
+
have an added `name` property. Its `BuiltinRule<File>` output supports File-typed
|
|
669
|
+
DTO fields. `blob()` accepts genuine Blob or File instances and returns
|
|
670
|
+
`BuiltinRule<Blob>`; use it when a filename is not required. Both use the
|
|
671
|
+
`node:buffer` classes, reject file-shaped objects and forged prototype-only
|
|
672
|
+
instances, and validate metadata without converting or cloning the value.
|
|
673
|
+
|
|
674
|
+
```ts
|
|
675
|
+
import type { Blob, File } from "node:buffer";
|
|
676
|
+
import { rules, type ValidationRules } from "@azure-net/node-kit/validation";
|
|
677
|
+
|
|
678
|
+
interface Upload {
|
|
679
|
+
attachment: File;
|
|
680
|
+
content: Blob;
|
|
681
|
+
}
|
|
682
|
+
const uploadRules: ValidationRules<Upload> = {
|
|
683
|
+
attachment: [rules.file({ maxSize: 5_000_000, extensions: ["pdf"] })],
|
|
684
|
+
content: [rules.blob({ maxSize: 5_000_000 })],
|
|
685
|
+
};
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Both rules share `ValueFileOptions`. `minSize`/`maxSize` are inclusive byte sizes.
|
|
689
|
+
`mimeTypes` contains exact
|
|
690
|
+
types or subtype wildcards such as `image/*`, matched case-insensitively against
|
|
691
|
+
the declared MIME essence while ignoring parameters such as charset.
|
|
692
|
+
`extensions` contains case-insensitive suffixes with or without a leading dot;
|
|
693
|
+
checking extensions requires a real File and uses its native name, including
|
|
694
|
+
when configured on `blob()`. Adding a name to a Blob does not satisfy this check.
|
|
695
|
+
Size bounds must be ordered nonnegative safe integers. Both rules are synchronous:
|
|
696
|
+
**no bytes are read,
|
|
697
|
+
no MIME signature/content inspection is performed**, and metadata is not proof
|
|
698
|
+
of safe content. Apply actual content/security checks separately.
|
|
699
|
+
|
|
700
|
+
### Strings, lengths, and patterns
|
|
701
|
+
|
|
702
|
+
| Rule | Semantics |
|
|
703
|
+
| ------------------------------------------------------------------ | ------------------------------------------------------------------ |
|
|
704
|
+
| `length(n, o?)`, `minLength(n, o?)`, `maxLength(n, o?)` | Exact/min/max size; `LengthOptions` |
|
|
705
|
+
| `matches(regex, o?)` | RegExp test, not necessarily a full-string match |
|
|
706
|
+
| `includes(text, o?)`, `startsWith(text, o?)`, `endsWith(text, o?)` | Case-sensitive substring/prefix/suffix checks |
|
|
707
|
+
| `lowercase(o?)`, `uppercase(o?)` | Value equals its JS lower/upper-case conversion; no transformation |
|
|
708
|
+
|
|
709
|
+
Lengths count Unicode code points by default, or UTF-16 units with
|
|
710
|
+
`{ unit: "codeUnits" }`; neither counts grapheme clusters. Arrays count elements,
|
|
711
|
+
Maps/Sets entries. Length bounds are nonnegative safe integers. Format/string
|
|
712
|
+
constraints should normally be paired with `string()`.
|
|
713
|
+
|
|
714
|
+
`matches` and URL filter patterns are copied with flags and reset internally;
|
|
715
|
+
they do not mutate caller `lastIndex`. Supply anchors if full matching is wanted.
|
|
716
|
+
Regex state-safety does not imply ReDoS safety: caller patterns must be trusted,
|
|
717
|
+
and JavaScript RegExp execution has no timeout here.
|
|
718
|
+
|
|
719
|
+
### String formats and scope
|
|
720
|
+
|
|
721
|
+
All entries below are nonstructural string constraints; add `string()`.
|
|
722
|
+
Unless noted, options are ordinary `RuleOptions`. These are format checks, not
|
|
723
|
+
existence, ownership, authenticity, allocation, or reachability checks.
|
|
724
|
+
|
|
725
|
+
| Rule | Accepted format and limitations |
|
|
726
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
727
|
+
| `email(o?)` | ASCII dot-atom local part up to 64 characters, dotted DNS domain, total up to 254; not all RFC 5322 mailboxes, no quoted local parts or deliverability check |
|
|
728
|
+
| `uuid(o?)` | RFC 9562 versions 1-8; `UuidOptions.version` restricts version; unrestricted also accepts Nil/Max UUIDs; case-insensitive |
|
|
729
|
+
| `guid(o?)` | Hyphenated `8-4-4-4-12` hex shape without UUID version/variant constraints |
|
|
730
|
+
| `url(o?)` | Absolute WHATWG URL, including non-HTTP schemes; `UrlOptions.protocol`/`hostname` RegExp filters |
|
|
731
|
+
| `httpUrl(o?)` | HTTP/HTTPS with explicit `://` and hostname; same URL filters; localhost/IPs can pass |
|
|
732
|
+
| `hostname(o?)` | ASCII DNS labels up to 63 characters, total name up to 253 excluding optional root dot; use punycode for IDNs |
|
|
733
|
+
| `e164(o?)` | `+` followed by 2-15 digits with nonzero first digit; not allocation/reachability verification |
|
|
734
|
+
| `emoji(o?)` | Common Unicode-property-based emoji sequences, flags, keycaps, modifiers, and joins; not a versioned RGI membership test |
|
|
735
|
+
| `base64(o?)` | Canonical padded RFC 4648 Base64; empty allowed |
|
|
736
|
+
| `base64url(o?)` | Canonical unpadded RFC 4648 URL alphabet; empty allowed |
|
|
737
|
+
| `hex(o?)` | Case-insensitive hex digits, including empty/odd-length strings; no `0x` prefix |
|
|
738
|
+
| `jwt(o?)` | Three nonempty canonical base64url segments; UTF-8 JSON object header/payload; syntax only; `JwtOptions.algorithm` optionally requires exact header `alg` |
|
|
739
|
+
| `nanoid(o?)` | URL alphabet letters/digits/underscore/hyphen, length 21 by default; positive safe-integer `NanoidOptions.length` |
|
|
740
|
+
| `cuid(o?)` | Canonical CUID v1 shape: `c` plus 24 lowercase alphanumerics |
|
|
741
|
+
| `cuid2(o?)` | Lowercase leading letter followed by zero or more lowercase alphanumerics; no fixed length |
|
|
742
|
+
| `ulid(o?)` | 26 case-insensitive Crockford-base32 characters, first digit `0..7` |
|
|
743
|
+
| `xid(o?)` | 20 case-insensitive base32hex characters `0-9a-v`; no padding-bit/provenance check |
|
|
744
|
+
| `ksuid(o?)` | 27 base62 characters; no decoded 160-bit range or timestamp check |
|
|
745
|
+
| `ip(o?)`, `ipv4(o?)`, `ipv6(o?)` | Node `net.isIP` address parsing; IPv6 zone identifiers rejected |
|
|
746
|
+
| `cidr(o?)`, `cidrv4(o?)`, `cidrv6(o?)` | Address/prefix notation, decimal prefix `0..32` or `0..128` without redundant leading zeros; host bits may be nonzero |
|
|
747
|
+
| `mac(o?)` | Six hex byte pairs; `MacOptions.delimiter` is `":"` (default), `"-"`, or `""`; case-insensitive |
|
|
748
|
+
| `creditCard(o?)` | 12-19 digits with optional single space/hyphen between digits and Luhn checksum; all-zero rejected; no issuer/account verification |
|
|
749
|
+
| `iban(o?)` | Uppercase electronic IBAN shape, 15-34 characters, check digits 02-98 and MOD-97; no country-specific length/BBAN/bank registry validation |
|
|
750
|
+
| `currencyCode(o?)` | Uppercase currency codes in host `Intl.supportedValuesOf("currency")`; ICU-dependent, not a live ISO registry |
|
|
751
|
+
| `hash(algorithm, o?)` | Exact digest length for `md5`, `sha1`, `sha256`, `sha384`, `sha512`; `HashOptions.encoding` is `hex` (default), `base64`, or `base64url`; no content hashing |
|
|
752
|
+
| `isoDate(o?)` | Gregorian `YYYY-MM-DD`, actual month/leap-day validation, including astronomical year 0000 |
|
|
753
|
+
| `isoTime(o?)` | Local `HH:mm[:ss[.fraction]]`; `IsoTimeOptions.precision` controls seconds/fraction; no leap seconds or 24:00 |
|
|
754
|
+
| `isoDateTime(o?)` | Date plus uppercase `T`, UTC `Z` by default; `IsoDateTimeOptions.offset`/`local` opt into offsets/local form |
|
|
755
|
+
| `isoDuration(o?)` | ISO 8601-1 subset: weeks alone, or ordered Y/M/D plus H/M/S; fractions only on seconds with dot/comma; no negative or mixed-week form |
|
|
756
|
+
|
|
757
|
+
URL checks reject whitespace, controls, and backslashes before parsing, but do
|
|
758
|
+
not constitute an SSRF or safe-navigation policy. URL filters see parsed protocol
|
|
759
|
+
without `:` and parsed hostname; the input itself is not rewritten.
|
|
760
|
+
|
|
761
|
+
JWT checks reject `alg: "none"`, require a nonempty string algorithm, allow an
|
|
762
|
+
absent `typ` or case-insensitive `JWT`, and reject `b64: false`. **No JWT signature
|
|
763
|
+
verification, claim validation, expiration check, or algorithm-security policy
|
|
764
|
+
is performed.** Use a verification library before trusting claims.
|
|
765
|
+
|
|
766
|
+
ISO time precision defaults to `null`: optional seconds/fractions for local time,
|
|
767
|
+
but seconds are required for zoned date-times. Precision `-1` requires minutes
|
|
768
|
+
only; `0` requires seconds without fraction; a positive safe integer requires
|
|
769
|
+
exactly that many fractional digits. `offset: true` permits `+/-HH:mm` through
|
|
770
|
+
23:59; `local: true` permits a zone-less form. These validate notation, not convert
|
|
771
|
+
to Date or resolve a time zone. ISO durations are not converted to elapsed time.
|
|
772
|
+
|
|
773
|
+
Implementation sources are the local [StringRules.ts](src/validation/rules/StringRules.ts)
|
|
774
|
+
and [format tests](tests/validation-strings.test.mjs): independent algorithms and
|
|
775
|
+
fixed patterns, Node `net.isIP`, WHATWG `URL`, Buffer encoding round trips, and host
|
|
776
|
+
Unicode/ICU data. The source records a format-scope review against Zod 4.6.7's
|
|
777
|
+
`src/v4/core/{regexes,schemas}.ts` (MIT, Colin McDonnell and contributors); that
|
|
778
|
+
is provenance for the review, not a claim of dependency or exact equivalence.
|
|
779
|
+
Standards named above describe the implemented scope, not blanket certification.
|
|
780
|
+
|
|
781
|
+
### Structures and collections
|
|
782
|
+
|
|
783
|
+
| Rule | Signature/semantics |
|
|
784
|
+
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
785
|
+
| `object(shape, o?)` | Typed field-to-rule-array shape; options also accept local `unknownKeys` |
|
|
786
|
+
| `array(itemRules, o?)` | Validate every element; empty array valid unless constrained |
|
|
787
|
+
| `tuple(ruleLists, o?)` | Positional rule arrays, exact length; no rest-element factory option |
|
|
788
|
+
| `record(valueRules, o?, keyRules?)` | Own enumerable string-key dictionary; optional key checks share each entry's path |
|
|
789
|
+
| `map(keyRules, valueRules, o?)` | Actual Map; paths `[entryIndex, "key"]` and `[entryIndex, "value"]` |
|
|
790
|
+
| `set(itemRules, o?)` | Actual Set; numeric paths by iteration order |
|
|
791
|
+
| `union(alternatives, o?)` | At least two rule-array alternatives; first successful branch wins |
|
|
792
|
+
| `discriminatedUnion(key, variants, o?)` | Nonempty string-keyed map of rule arrays; own data-property tag selects branch; selected-branch errors retain precise nested paths |
|
|
793
|
+
| `intersection(leftShape, rightShape, o?)` | Merge two object rule maps; shared fields must satisfy both rule lists independently; not arbitrary schema intersection |
|
|
794
|
+
| `validator(ChildValidator, o?)` | Reuse child definition with child root boundary |
|
|
795
|
+
| `lazy(() => ruleArray, o?)` | Resolve once, cache rules; supports recursive definitions |
|
|
796
|
+
|
|
797
|
+
The discriminator may be a string, number, or boolean, matched by `String(tag)`
|
|
798
|
+
against own variant keys. Missing/inherited/accessor/unknown tags produce a generic
|
|
799
|
+
`type.union` issue at the union field; getters are not invoked. Once a variant is
|
|
800
|
+
selected, its failures are reported directly: for a field `payload` with a selected
|
|
801
|
+
object branch containing `name`, a bad name reports at `["payload", "name"]`,
|
|
802
|
+
not just `["payload"]`. Ordinary `union()` still reports a generic union-field
|
|
803
|
+
failure when no alternative succeeds. Include a structural guard within each branch. A lazy resolver
|
|
804
|
+
must return valid rule arrays, not request-dependent data or a promise.
|
|
805
|
+
|
|
806
|
+
### Collection constraints, comparisons, and conditions
|
|
807
|
+
|
|
808
|
+
| Rule | Semantics |
|
|
809
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
810
|
+
| `unique(o?)` | Array elements unique by Set/SameValueZero semantics; objects compared by identity |
|
|
811
|
+
| `uniqueBy<T>(key: keyof T, o?)` | Array values at own selected property unique by Set semantics; absent properties select undefined |
|
|
812
|
+
| `contains(expected, o?)` | At least one array element deep-strict-equal to expected |
|
|
813
|
+
| `some<T>(predicate, o?)` | Callback using array `.some`; predicate `(value: T, index: number) => boolean` |
|
|
814
|
+
| `every<T>(predicate, o?)` | Callback using array `.every`; same predicate type; empty array succeeds |
|
|
815
|
+
| `sameAs(key, o?)` | Current value `Object.is`-equal to an own sibling property |
|
|
816
|
+
| `notSameAs(key, o?)` | Current value not `Object.is`-equal to an own sibling property |
|
|
817
|
+
| `when(condition, checks)` | Conditional value checks/callbacks; no separate options argument |
|
|
818
|
+
|
|
819
|
+
`sameAs`/`notSameAs` both fail when the sibling is absent. They are not deep
|
|
820
|
+
equality: two distinct equal-looking objects differ, NaN equals itself, and
|
|
821
|
+
`+0` differs from `-0`. Use `equal`/`notEqual` for deep comparison to a configured
|
|
822
|
+
value or a custom callback for root-dependent comparisons.
|
|
823
|
+
|
|
824
|
+
`when<T, D = unknown, R = D, C = unknown>` takes a synchronous
|
|
825
|
+
`(context: RuleContext<T, D, R, C>) => boolean` and an array of
|
|
826
|
+
`FieldRule<T, D, R, C>`. A false condition succeeds without checks. A true condition
|
|
827
|
+
runs them sequentially and stops at the first failure, independently of the outer
|
|
828
|
+
cascade. It may return a promise if a contained callback does. Structural rules,
|
|
829
|
+
markers, objects, unions, and other nested descriptors inside `when` throw at
|
|
830
|
+
construction; put structure outside the condition. `some`/`every` predicates are
|
|
831
|
+
synchronous; async predicates are not awaited. All three are user callbacks for
|
|
832
|
+
the engine's callback-suppression rules.
|
|
833
|
+
|
|
834
|
+
## Differences from Zod and deliberate non-goals
|
|
835
|
+
|
|
836
|
+
This is not a claim of total Zod parity. It is a model-first validator with arrays
|
|
837
|
+
of descriptors/callbacks and static class methods, not a fluent schema builder.
|
|
838
|
+
It offers no transforms, defaults, coercion, codecs, preprocessing pipeline,
|
|
839
|
+
schema-driven output cloning, or JSON Schema builder/exporter. Unknown keys reject
|
|
840
|
+
by default, and allowing them preserves them. Structural guards must be explicit;
|
|
841
|
+
format constraints alone are not structural schemas.
|
|
842
|
+
|
|
843
|
+
Length defaults to code points, not UTF-16 units. `multipleOf` uses exact decimal
|
|
844
|
+
representation rather than epsilon acceptance. `float32` is range-only; 64-bit
|
|
845
|
+
integers require bigint. Decimal strings stay strings. Date rules require Dates,
|
|
846
|
+
while ISO rules validate strings. Ordinary union errors are compact, selected
|
|
847
|
+
discriminated-union errors preserve nested paths, intersection merges object
|
|
848
|
+
shapes only, and typed callbacks run after structural preparation rather
|
|
849
|
+
than in arbitrary array order. Format catalogs have the explicit limits above;
|
|
850
|
+
similar names do not guarantee interchangeable acceptance sets or error shapes.
|
|
851
|
+
|
|
852
|
+
## Source and test map
|
|
853
|
+
|
|
854
|
+
- [Public types](src/validation/Types.ts), [Validator](src/validation/Validator.ts),
|
|
855
|
+
[engine](src/validation/Engine.ts), and [results](src/validation/Result.ts).
|
|
856
|
+
- [Rule exports and typed facade](src/validation/rules/index.ts),
|
|
857
|
+
[value rules](src/validation/rules/ValueRules.ts),
|
|
858
|
+
[structure rules](src/validation/rules/StructureRules.ts), and
|
|
859
|
+
[custom rules](src/validation/rules/CustomRules.ts).
|
|
860
|
+
- [Message exports](src/validation/messages/index.ts) and
|
|
861
|
+
[HTTP response builder](src/delivery/http/ResponseBuilder.ts).
|
|
862
|
+
- [Engine tests](tests/validation-engine.test.mjs),
|
|
863
|
+
[presence-order tests](tests/validation-presence.test.mjs),
|
|
864
|
+
[type tests](tests/validation.types.ts), [result tests](tests/validation-result.test.mjs),
|
|
865
|
+
[value tests](tests/validation-values.test.mjs), and
|
|
866
|
+
[string-format tests](tests/validation-strings.test.mjs).
|