@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +301 -2
  4. package/VALIDATION.md +866 -0
  5. package/dist/delivery/http/ResponseBuilder.d.ts +44 -0
  6. package/dist/delivery/http/ResponseBuilder.js +135 -0
  7. package/dist/delivery/http/index.d.ts +1 -0
  8. package/dist/delivery/http/index.js +1 -0
  9. package/dist/delivery/index.d.ts +1 -0
  10. package/dist/delivery/index.js +1 -0
  11. package/dist/domain/Ensure.d.ts +2 -0
  12. package/dist/domain/Ensure.js +7 -0
  13. package/dist/domain/EnsureError.d.ts +4 -0
  14. package/dist/domain/EnsureError.js +7 -0
  15. package/dist/domain/index.d.ts +2 -0
  16. package/dist/domain/index.js +2 -0
  17. package/dist/infrastructure/background/BackgroundTask.d.ts +12 -0
  18. package/dist/infrastructure/background/BackgroundTask.js +41 -0
  19. package/dist/infrastructure/background/index.d.ts +1 -0
  20. package/dist/infrastructure/background/index.js +1 -0
  21. package/dist/infrastructure/environment/Env.d.ts +19 -0
  22. package/dist/infrastructure/environment/Env.js +33 -0
  23. package/dist/infrastructure/environment/index.d.ts +1 -0
  24. package/dist/infrastructure/environment/index.js +1 -0
  25. package/dist/infrastructure/index.d.ts +2 -0
  26. package/dist/infrastructure/index.js +2 -0
  27. package/dist/tools/DateHelper.d.ts +13 -0
  28. package/dist/tools/DateHelper.js +77 -0
  29. package/dist/tools/DurationHelper.d.ts +6 -0
  30. package/dist/tools/DurationHelper.js +31 -0
  31. package/dist/tools/EnumHelper.d.ts +26 -0
  32. package/dist/tools/EnumHelper.js +40 -0
  33. package/dist/tools/ErrorHelper.d.ts +3 -0
  34. package/dist/tools/ErrorHelper.js +31 -0
  35. package/dist/tools/NumberHelper.d.ts +19 -0
  36. package/dist/tools/NumberHelper.js +49 -0
  37. package/dist/tools/ObjectHelper.d.ts +28 -0
  38. package/dist/tools/ObjectHelper.js +90 -0
  39. package/dist/tools/PromiseHelper.d.ts +3 -0
  40. package/dist/tools/PromiseHelper.js +7 -0
  41. package/dist/tools/StringHelper.d.ts +75 -0
  42. package/dist/tools/StringHelper.js +127 -0
  43. package/dist/tools/UidHelper.d.ts +56 -0
  44. package/dist/tools/UidHelper.js +132 -0
  45. package/dist/tools/index.d.ts +9 -0
  46. package/dist/tools/index.js +9 -0
  47. package/dist/validation/Engine.d.ts +6 -0
  48. package/dist/validation/Engine.js +406 -0
  49. package/dist/validation/Result.d.ts +31 -0
  50. package/dist/validation/Result.js +264 -0
  51. package/dist/validation/Symbols.d.ts +1 -0
  52. package/dist/validation/Symbols.js +1 -0
  53. package/dist/validation/Types.d.ts +105 -0
  54. package/dist/validation/Types.js +1 -0
  55. package/dist/validation/Validator.d.ts +26 -0
  56. package/dist/validation/Validator.js +95 -0
  57. package/dist/validation/index.d.ts +5 -0
  58. package/dist/validation/index.js +5 -0
  59. package/dist/validation/messages/En.d.ts +3 -0
  60. package/dist/validation/messages/En.js +118 -0
  61. package/dist/validation/messages/Ru.d.ts +3 -0
  62. package/dist/validation/messages/Ru.js +118 -0
  63. package/dist/validation/messages/index.d.ts +7 -0
  64. package/dist/validation/messages/index.js +7 -0
  65. package/dist/validation/rules/CustomRules.d.ts +8 -0
  66. package/dist/validation/rules/CustomRules.js +64 -0
  67. package/dist/validation/rules/Factory.d.ts +4 -0
  68. package/dist/validation/rules/Factory.js +21 -0
  69. package/dist/validation/rules/StringRules.d.ts +105 -0
  70. package/dist/validation/rules/StringRules.js +481 -0
  71. package/dist/validation/rules/StructureRules.d.ts +30 -0
  72. package/dist/validation/rules/StructureRules.js +145 -0
  73. package/dist/validation/rules/ValueRules.d.ts +82 -0
  74. package/dist/validation/rules/ValueRules.js +365 -0
  75. package/dist/validation/rules/index.d.ts +151 -0
  76. package/dist/validation/rules/index.js +19 -0
  77. package/package.json +77 -3
@@ -0,0 +1,264 @@
1
+ import { types } from "node:util";
2
+ /**
3
+ * JSON metadata only: bigint -> decimal string, Date -> ISO (invalid -> null),
4
+ * RegExp -> /source/flags, non-finite numbers -> null. Only own enumerable data
5
+ * properties are copied; accessors, functions, symbols, undefined and proxies
6
+ * are omitted (null in arrays). No getters or toJSON methods are called.
7
+ * Cycles become "[Circular]". Depth (32), container width (1000), and shared
8
+ * per-result budgets (10000 entries / 100000 characters) bound the snapshot.
9
+ * Truncated values become "[Truncated]"; oversized keys are omitted. Objects
10
+ * use a $snapshot marker when their contents cannot be represented in budget.
11
+ */
12
+ function snapshotMetadata(value, budget, active = new WeakSet(), depth = 0) {
13
+ if (budget.entries-- <= 0 || depth > 32)
14
+ return "[Truncated]";
15
+ const text = (input) => {
16
+ const length = Math.min(input.length, 4096, Math.max(0, budget.characters));
17
+ budget.characters -= length;
18
+ return (input.slice(0, length) + (length < input.length ? "[Truncated]" : ""));
19
+ };
20
+ if (value === null || typeof value === "boolean")
21
+ return value;
22
+ if (typeof value === "string")
23
+ return text(value);
24
+ if (typeof value === "bigint")
25
+ return text(String(value));
26
+ if (typeof value === "number")
27
+ return Number.isFinite(value) ? value : null;
28
+ if (typeof value !== "object" || types.isProxy(value))
29
+ return undefined;
30
+ if (active.has(value))
31
+ return "[Circular]";
32
+ if (types.isDate(value)) {
33
+ const time = Date.prototype.getTime.call(value);
34
+ return Number.isFinite(time)
35
+ ? text(Date.prototype.toISOString.call(value))
36
+ : null;
37
+ }
38
+ if (types.isRegExp(value)) {
39
+ const source = Object.getOwnPropertyDescriptor(RegExp.prototype, "source").get.call(value);
40
+ let flags = "";
41
+ for (const [property, flag] of [
42
+ ["hasIndices", "d"],
43
+ ["global", "g"],
44
+ ["ignoreCase", "i"],
45
+ ["multiline", "m"],
46
+ ["dotAll", "s"],
47
+ ["unicode", "u"],
48
+ ["unicodeSets", "v"],
49
+ ["sticky", "y"],
50
+ ]) {
51
+ if (Object.getOwnPropertyDescriptor(RegExp.prototype, property)?.get?.call(value))
52
+ flags += flag;
53
+ }
54
+ return text(`/${source}/${flags}`);
55
+ }
56
+ active.add(value);
57
+ try {
58
+ if (Array.isArray(value)) {
59
+ const length = Object.getOwnPropertyDescriptor(value, "length")
60
+ .value;
61
+ const result = [];
62
+ for (let i = 0; i < Math.min(length, 1000); i++) {
63
+ if (budget.entries <= 0)
64
+ break;
65
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
66
+ result.push(snapshotMetadata(descriptor && "value" in descriptor ? descriptor.value : undefined, budget, active, depth + 1) ?? null);
67
+ }
68
+ if (result.length < length)
69
+ result.push("[Truncated]");
70
+ return Object.freeze(result);
71
+ }
72
+ const result = {};
73
+ let count = 0;
74
+ for (const key of Reflect.ownKeys(value)) {
75
+ if (budget.entries <= 0 || count++ >= 1000 || budget.characters <= 0) {
76
+ Object.defineProperty(result, "$snapshot", {
77
+ value: "[Truncated]",
78
+ enumerable: true,
79
+ configurable: true,
80
+ });
81
+ break;
82
+ }
83
+ if (typeof key !== "string" ||
84
+ key.length > 4096 ||
85
+ key.length > budget.characters)
86
+ continue;
87
+ budget.characters -= key.length;
88
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
89
+ if (!descriptor?.enumerable || !("value" in descriptor))
90
+ continue;
91
+ const item = snapshotMetadata(descriptor.value, budget, active, depth + 1);
92
+ if (item !== undefined)
93
+ Object.defineProperty(result, key, {
94
+ value: item,
95
+ enumerable: true,
96
+ configurable: true,
97
+ });
98
+ }
99
+ return Object.freeze(result);
100
+ }
101
+ finally {
102
+ active.delete(value);
103
+ }
104
+ }
105
+ function snapshotParams(params, budget) {
106
+ const snapshot = snapshotMetadata(params, budget);
107
+ return snapshot !== null && typeof snapshot === "object"
108
+ ? snapshot
109
+ : Object.freeze({ $snapshot: snapshot ?? "[Omitted]" });
110
+ }
111
+ // Public paths need limits independently of the engine's traversal budget.
112
+ const MAX_ISSUES = 10_000;
113
+ const MAX_DEPTH = 128;
114
+ const MAX_TREE_NODES = 100_000;
115
+ const MAX_ARRAY_SLOTS = 100_000;
116
+ const MAX_INDEX = 10_000;
117
+ function snapshotIssues(issues) {
118
+ if (issues.length > MAX_ISSUES)
119
+ throw new RangeError("Too many validation issues");
120
+ const root = { arrayLength: 0 };
121
+ let nodes = 1;
122
+ let slots = 0;
123
+ const metadataBudget = {
124
+ entries: 10_000,
125
+ characters: 100_000,
126
+ };
127
+ return Object.freeze(issues.map((issue) => {
128
+ if (issue.path.length > MAX_DEPTH)
129
+ throw new RangeError("Validation path is too deep");
130
+ let node = root;
131
+ for (const part of issue.path) {
132
+ if (typeof part === "number") {
133
+ if (!Number.isSafeInteger(part) || part < 0 || part > MAX_INDEX) {
134
+ throw new RangeError("Validation array index must be an integer between 0 and 10000");
135
+ }
136
+ // Count each array's high-water mark, not each occurrence of its path.
137
+ const length = Math.max(node.arrayLength, part + 1);
138
+ slots += length - node.arrayLength;
139
+ node.arrayLength = length;
140
+ if (slots > MAX_ARRAY_SLOTS)
141
+ throw new RangeError("Validation array paths are too large");
142
+ }
143
+ else if (typeof part !== "string") {
144
+ throw new TypeError("Validation path segments must be strings or numbers");
145
+ }
146
+ const children = (node.children ??= new Map());
147
+ let child = children.get(part);
148
+ if (!child) {
149
+ if (++nodes > MAX_TREE_NODES)
150
+ throw new RangeError("Validation error tree is too large");
151
+ child = { arrayLength: 0 };
152
+ children.set(part, child);
153
+ }
154
+ node = child;
155
+ }
156
+ return Object.freeze({
157
+ path: Object.freeze([...issue.path]),
158
+ code: issue.code,
159
+ message: issue.message,
160
+ params: snapshotParams(issue.params, metadataBudget),
161
+ });
162
+ }));
163
+ }
164
+ function freezeTree(node) {
165
+ if (node.fields) {
166
+ for (const child of Object.values(node.fields))
167
+ freezeTree(child);
168
+ Object.freeze(node.fields);
169
+ }
170
+ if (node.items) {
171
+ for (const child of node.items)
172
+ if (child)
173
+ freezeTree(child);
174
+ Object.freeze(node.items);
175
+ }
176
+ Object.freeze(node.messages);
177
+ Object.freeze(node);
178
+ }
179
+ /** A snapshot of validation diagnostics. Failed results never retain `data`. */
180
+ export class ValidationResult {
181
+ isValid;
182
+ issues;
183
+ data;
184
+ #errors;
185
+ /** No issues means success (including when data is undefined); otherwise data is discarded. */
186
+ constructor(data, issues) {
187
+ this.issues = snapshotIssues(issues);
188
+ this.isValid = this.issues.length === 0;
189
+ this.data = this.isValid ? data : undefined;
190
+ Object.freeze(this);
191
+ }
192
+ get errors() {
193
+ if (this.#errors)
194
+ return this.#errors;
195
+ const root = { messages: [] };
196
+ for (const issue of this.issues) {
197
+ let node = root;
198
+ for (const part of issue.path) {
199
+ if (typeof part === "number") {
200
+ const items = (node.items ??= []);
201
+ while (items.length <= part)
202
+ items.push(null);
203
+ node = items[part] ??= { messages: [] };
204
+ }
205
+ else {
206
+ const fields = (node.fields ??= Object.create(null));
207
+ node = fields[part] ??= { messages: [] };
208
+ }
209
+ }
210
+ node.messages.push(issue.message);
211
+ }
212
+ freezeTree(root);
213
+ this.#errors = root;
214
+ return this.#errors;
215
+ }
216
+ get formErrors() {
217
+ return this.errors.messages;
218
+ }
219
+ /**
220
+ * Flat RFC 6901 JSON Pointer keys: ~ becomes ~0, / becomes ~1.
221
+ * Root issues are excluded (see formErrors). String "0" and numeric 0
222
+ * share a pointer; the typed tree preserves their separate namespaces.
223
+ */
224
+ toFieldErrors() {
225
+ const fields = Object.create(null);
226
+ for (const issue of this.issues) {
227
+ if (!issue.path.length)
228
+ continue;
229
+ const pointer = "/" +
230
+ issue.path
231
+ .map((part) => String(part).replace(/~/g, "~0").replace(/\//g, "~1"))
232
+ .join("/");
233
+ (fields[pointer] ??= []).push(issue.message);
234
+ }
235
+ for (const messages of Object.values(fields))
236
+ Object.freeze(messages);
237
+ return Object.freeze(fields);
238
+ }
239
+ }
240
+ /** An Error wrapper whose JSON representation excludes validated input and stack. */
241
+ export class ValidationError extends Error {
242
+ result;
243
+ constructor(result, message = "Validation failed") {
244
+ super(message);
245
+ if (result.isValid)
246
+ throw new TypeError("ValidationError requires a failed validation result");
247
+ this.name = "ValidationError";
248
+ this.result = result;
249
+ }
250
+ get issues() {
251
+ return this.result.issues;
252
+ }
253
+ get errors() {
254
+ return this.result.errors;
255
+ }
256
+ toJSON() {
257
+ return {
258
+ name: this.name,
259
+ message: this.message,
260
+ issues: this.issues,
261
+ errors: this.errors,
262
+ };
263
+ }
264
+ }
@@ -0,0 +1 @@
1
+ export declare const validatorDefinition: unique symbol;
@@ -0,0 +1 @@
1
+ export const validatorDefinition = Symbol("Validator.definition");
@@ -0,0 +1,105 @@
1
+ export type ValidationPath = readonly (string | number)[];
2
+ export type ValidationParams = Readonly<Record<string, unknown>>;
3
+ export interface MessageContext {
4
+ readonly code: string;
5
+ readonly messageKey: string;
6
+ readonly params: ValidationParams;
7
+ readonly path: ValidationPath;
8
+ readonly locale: string;
9
+ readonly label: string;
10
+ }
11
+ export type ValidationMessage = string | ((context: MessageContext) => string);
12
+ export type ValidationMessages = Readonly<Record<string, ValidationMessage>>;
13
+ export type ValidationLocales = Readonly<Record<string, ValidationMessages>>;
14
+ export interface RuleOptions {
15
+ readonly message?: ValidationMessage;
16
+ readonly messageKey?: string;
17
+ readonly code?: string;
18
+ readonly label?: string;
19
+ }
20
+ export interface RuleFailure extends RuleOptions {
21
+ readonly params?: ValidationParams;
22
+ /** Relative to the current field. Omit to report against that field. */
23
+ readonly path?: ValidationPath;
24
+ }
25
+ export type RuleResult = boolean | RuleFailure | readonly RuleFailure[];
26
+ export interface RuleContext<TValue, TData = unknown, TRoot = TData, TContext = unknown> {
27
+ readonly value: TValue;
28
+ readonly data: TData;
29
+ readonly root: TRoot;
30
+ readonly context: TContext;
31
+ readonly path: ValidationPath;
32
+ readonly signal: AbortSignal | undefined;
33
+ }
34
+ export type SyncValidationRule<TValue, TData = unknown, TRoot = TData, TContext = unknown> = (context: RuleContext<TValue, TData, TRoot, TContext>) => RuleResult;
35
+ export type AsyncValidationRule<TValue, TData = unknown, TRoot = TData, TContext = unknown> = (context: RuleContext<TValue, TData, TRoot, TContext>) => PromiseLike<RuleResult>;
36
+ export type ValidationRule<TValue, TData = unknown, TRoot = TData, TContext = unknown> = (context: RuleContext<TValue, TData, TRoot, TContext>) => RuleResult | PromiseLike<RuleResult>;
37
+ /** Built-in descriptors are immutable. The phantom output preserves field compatibility. */
38
+ export interface BuiltinRule<TValue = unknown, TContext = unknown, TRoot = unknown> {
39
+ readonly kind: "test" | "optional" | "nullable" | "object" | "array" | "tuple" | "record" | "map" | "set" | "union" | "intersection" | "lazy" | "validator";
40
+ readonly code: string;
41
+ readonly structural: boolean;
42
+ readonly options: RuleOptions;
43
+ readonly params: ValidationParams;
44
+ readonly test?: (value: unknown, context: RuleContext<unknown>) => RuleResult;
45
+ readonly shape?: Readonly<Record<string, readonly AnyRule[]>>;
46
+ readonly items?: readonly AnyRule[];
47
+ readonly keys?: readonly AnyRule[];
48
+ readonly tuple?: readonly (readonly AnyRule[])[];
49
+ readonly rest?: readonly AnyRule[];
50
+ readonly alternatives?: readonly (readonly AnyRule[])[];
51
+ readonly discriminator?: string;
52
+ readonly variants?: Readonly<Record<string, readonly AnyRule[]>>;
53
+ readonly resolve?: () => readonly AnyRule[];
54
+ readonly unknownKeys?: "reject" | "allow";
55
+ readonly output?: () => TValue;
56
+ readonly needsContext?: (context: TContext) => void;
57
+ readonly needsRoot?: (root: TRoot) => void;
58
+ }
59
+ export type FieldRule<TValue, TData = unknown, TRoot = TData, TContext = unknown> = BuiltinRule<TValue, TContext, TRoot> | ValidationRule<TValue, TData, TRoot, TContext>;
60
+ export type ValidationRules<T extends object, TRoot = T, TContext = unknown> = {
61
+ readonly [K in keyof T]-?: K extends string | number ? readonly (BuiltinRule<T[K], TContext, TRoot> | ValidationRule<NoInfer<T[K]>, NoInfer<T>, TRoot, TContext>)[] : never;
62
+ };
63
+ /** Internal erased rule representation; public callbacks retain their contextual types. */
64
+ export type AnyRule = BuiltinRule<any, any, any> | ValidationRule<any, any, any, any>;
65
+ export interface ValidationIssue {
66
+ readonly path: ValidationPath;
67
+ readonly code: string;
68
+ readonly message: string;
69
+ readonly params: ValidationParams;
70
+ }
71
+ /** Separate namespaces avoid collisions with fields named messages, fields or items. */
72
+ export type ValidationErrors<T = unknown> = {
73
+ readonly messages: readonly string[];
74
+ readonly fields?: unknown extends T ? Readonly<Record<string, ValidationErrors>> : NonNullable<T> extends readonly unknown[] | Map<unknown, unknown> | Set<unknown> ? never : NonNullable<T> extends object ? {
75
+ readonly [K in keyof NonNullable<T>]?: ValidationErrors<NonNullable<T>[K]>;
76
+ } : never;
77
+ readonly items?: unknown extends T ? readonly (ValidationErrors | null)[] : NonNullable<T> extends readonly (infer U)[] ? readonly (ValidationErrors<U> | null)[] : NonNullable<T> extends Map<infer K, infer V> ? readonly (ValidationErrors<{
78
+ key: K;
79
+ value: V;
80
+ }> | null)[] : NonNullable<T> extends Set<infer U> ? readonly (ValidationErrors<U> | null)[] : never;
81
+ };
82
+ export interface ValidatorOptions {
83
+ readonly messages?: ValidationMessages;
84
+ readonly locales?: ValidationLocales;
85
+ readonly defaultLocale?: string;
86
+ readonly fallbackLocale?: string;
87
+ readonly getLocale?: () => string | undefined;
88
+ readonly maxIssues?: number;
89
+ readonly maxDepth?: number;
90
+ readonly maxNodes?: number;
91
+ readonly unknownKeys?: "reject" | "allow";
92
+ readonly cascade?: "stop" | "continue";
93
+ }
94
+ export interface ValidatorCreateOptions extends Omit<ValidatorOptions, "messages"> {
95
+ /** A single-language catalog or catalogs keyed by locale. */
96
+ readonly messages?: ValidationMessages | ValidationLocales;
97
+ }
98
+ export interface ValidationOptions<TContext = unknown> {
99
+ readonly locale?: string;
100
+ readonly context?: TContext;
101
+ readonly signal?: AbortSignal;
102
+ }
103
+ export type ValidationArguments<TContext> = unknown extends TContext ? [options?: ValidationOptions<TContext>] : [options: ValidationOptions<TContext> & {
104
+ readonly context: TContext;
105
+ }];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import type { ValidationResult } from "./Result.js";
2
+ import type { AnyRule, ValidationArguments, ValidationRules, ValidatorOptions, ValidatorCreateOptions } from "./Types.js";
3
+ import { validatorDefinition } from "./Symbols.js";
4
+ type Shape = Readonly<Record<string, readonly AnyRule[]>>;
5
+ type ValidatorConstructor<T extends object, C> = (new () => Validator<T, C>) & {
6
+ readonly configuration: ValidatorOptions;
7
+ };
8
+ export declare abstract class Validator<T extends object, TContext = unknown> {
9
+ static readonly configuration: ValidatorOptions;
10
+ /** Called once per subclass, after construction. Never capture request-local state here. */
11
+ protected abstract rules(): ValidationRules<T, T, TContext>;
12
+ private static config;
13
+ private static schema;
14
+ static [validatorDefinition]<T extends object, C>(this: ValidatorConstructor<T, C>): Shape;
15
+ static validate<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): ValidationResult<T>;
16
+ static validateAndThrow<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): T;
17
+ static validateAsync<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): Promise<ValidationResult<T>>;
18
+ static validateAndThrowAsync<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): Promise<T>;
19
+ /** Both Validator.create(options) and new Validator.create(options) return a base class. */
20
+ static readonly create: {
21
+ (options?: ValidatorCreateOptions): typeof Validator;
22
+ new (options?: ValidatorCreateOptions): typeof Validator;
23
+ };
24
+ }
25
+ export declare const createValidator: (options?: ValidatorCreateOptions) => typeof Validator;
26
+ export {};
@@ -0,0 +1,95 @@
1
+ import { validateAsync, validateSync } from "./Engine.js";
2
+ import { ValidationError } from "./Result.js";
3
+ import { validatorDefinition } from "./Symbols.js";
4
+ const schemas = new WeakMap();
5
+ const configurations = new WeakMap();
6
+ function settings(options) {
7
+ for (const [key, limit] of [
8
+ ["maxDepth", 64],
9
+ ["maxNodes", 10000],
10
+ ["maxIssues", 10000],
11
+ ]) {
12
+ const value = options[key];
13
+ if (value !== undefined &&
14
+ (!Number.isSafeInteger(value) || value < 1 || value > limit)) {
15
+ throw new RangeError(`${key} must be an integer between 1 and ${limit}.`);
16
+ }
17
+ }
18
+ const entries = Object.values(options.messages ?? {});
19
+ const multi = entries.some((value) => value !== null && typeof value === "object");
20
+ if (multi &&
21
+ entries.some((value) => value === null || typeof value !== "object" || Array.isArray(value))) {
22
+ throw new TypeError("messages must be a flat catalog or a locale-to-catalog map, not a mixture.");
23
+ }
24
+ const messages = multi
25
+ ? undefined
26
+ : options.messages;
27
+ const locales = options.locales ??
28
+ (multi ? options.messages : undefined);
29
+ return Object.freeze({
30
+ ...options,
31
+ messages: messages && Object.freeze({ ...messages }),
32
+ locales: locales &&
33
+ Object.freeze(Object.fromEntries(Object.entries(locales).map(([locale, messages]) => [
34
+ locale,
35
+ Object.freeze({ ...messages }),
36
+ ]))),
37
+ });
38
+ }
39
+ export class Validator {
40
+ static configuration = Object.freeze({});
41
+ static config(constructor) {
42
+ let config = configurations.get(constructor);
43
+ if (!config) {
44
+ config = settings(constructor.configuration);
45
+ configurations.set(constructor, config);
46
+ }
47
+ return config;
48
+ }
49
+ static schema(constructor) {
50
+ let schema = schemas.get(constructor);
51
+ if (!schema) {
52
+ const definition = new constructor().rules();
53
+ if (Object.getOwnPropertySymbols(definition).length)
54
+ throw new TypeError("Validation schemas support string and numeric keys, not symbols.");
55
+ schema = Object.freeze(Object.fromEntries(Object.entries(definition).map(([key, rules]) => {
56
+ if (!Array.isArray(rules))
57
+ throw new TypeError(`Rules for ${key} must be an array.`);
58
+ return [key, Object.freeze([...rules])];
59
+ })));
60
+ schemas.set(constructor, schema);
61
+ }
62
+ return schema;
63
+ }
64
+ static [validatorDefinition]() {
65
+ Validator.config(this);
66
+ return Validator.schema(this);
67
+ }
68
+ static validate(input, ...[options]) {
69
+ return validateSync(input, Validator.schema(this), Validator.config(this), options ?? {});
70
+ }
71
+ static validateAndThrow(input, ...[options]) {
72
+ const result = validateSync(input, Validator.schema(this), Validator.config(this), options ?? {});
73
+ if (!result.isValid)
74
+ throw new ValidationError(result);
75
+ return result.data;
76
+ }
77
+ static async validateAsync(input, ...[options]) {
78
+ return validateAsync(input, Validator.schema(this), Validator.config(this), options ?? {});
79
+ }
80
+ static async validateAndThrowAsync(input, ...[options]) {
81
+ const result = await validateAsync(input, Validator.schema(this), Validator.config(this), options ?? {});
82
+ if (!result.isValid)
83
+ throw new ValidationError(result);
84
+ return result.data;
85
+ }
86
+ /** Both Validator.create(options) and new Validator.create(options) return a base class. */
87
+ static create = function (options = {}) {
88
+ const configuration = settings(options);
89
+ class ConfiguredValidator extends Validator {
90
+ static configuration = configuration;
91
+ }
92
+ return ConfiguredValidator;
93
+ };
94
+ }
95
+ export const createValidator = (options = {}) => Validator.create(options);
@@ -0,0 +1,5 @@
1
+ export * from "./Types.js";
2
+ export * from "./Result.js";
3
+ export * from "./Validator.js";
4
+ export * from "./rules/index.js";
5
+ export * from "./messages/index.js";
@@ -0,0 +1,5 @@
1
+ export * from "./Types.js";
2
+ export * from "./Result.js";
3
+ export * from "./Validator.js";
4
+ export * from "./rules/index.js";
5
+ export * from "./messages/index.js";
@@ -0,0 +1,3 @@
1
+ import type { ValidationMessages } from "../Types.js";
2
+ /** Flat, dependency-free English catalog keyed by validation code. */
3
+ export declare const validationMessagesEn: ValidationMessages;
@@ -0,0 +1,118 @@
1
+ /** Flat, dependency-free English catalog keyed by validation code. */
2
+ export const validationMessagesEn = Object.freeze({
3
+ "type.object": "Must be an object.",
4
+ "type.array": "Must be an array.",
5
+ "type.tuple": "Must match the required tuple structure.",
6
+ "type.record": "Must be a record.",
7
+ "type.map": "Must be a Map.",
8
+ "type.set": "Must be a Set.",
9
+ "type.union": "Must match one of the allowed alternatives.",
10
+ "type.intersection": "Must satisfy all intersected validation rules.",
11
+ "type.lazy": "Must match the resolved validation rules.",
12
+ "type.validator": "Must satisfy the nested validator.",
13
+ "object.unknownKeys": "Contains unknown fields.",
14
+ "object.accessor": "Accessor properties are not supported.",
15
+ "validation.depth": "Maximum validation depth exceeded.",
16
+ "validation.nodes": "Maximum validation node count exceeded.",
17
+ "validation.cycle": "Circular references are not supported.",
18
+ "custom.invalid": "Invalid value.",
19
+ "array.unique": "Array items must be unique.",
20
+ "array.length": ({ params }) => `Array must contain exactly ${String(params.length)} items.`,
21
+ "array.contains": "Array must contain the required value.",
22
+ "array.some": "At least one array item must satisfy the condition.",
23
+ "array.every": "Every array item must satisfy the condition.",
24
+ "comparison.sameAs": "Must match the referenced field.",
25
+ "comparison.notSameAs": "Must differ from the referenced field.",
26
+ string: "Must be a string.",
27
+ number: "Must be a finite number.",
28
+ finite: "Must be a finite number.",
29
+ bigint: "Must be a bigint.",
30
+ boolean: "Must be a boolean.",
31
+ date: "Must be a valid Date.",
32
+ symbol: "Must be a symbol.",
33
+ literal: "Must equal the required literal value.",
34
+ enum: "Must be an allowed enum value.",
35
+ enumName: "Must be an allowed enum name.",
36
+ integer: "Must be an integer.",
37
+ safeInteger: "Must be a safe integer.",
38
+ int32: "Must be a signed 32-bit integer.",
39
+ uint32: "Must be an unsigned 32-bit integer.",
40
+ float32: "Must be a finite number within the 32-bit float range.",
41
+ int64: "Must be a signed 64-bit bigint.",
42
+ uint64: "Must be an unsigned 64-bit bigint.",
43
+ lessThan: ({ params }) => `Must be ${params.inclusive === true ? "at most" : "less than"} ${String(params.max)}.`,
44
+ greaterThan: ({ params }) => `Must be ${params.inclusive === true ? "at least" : "greater than"} ${String(params.min)}.`,
45
+ lessThanOrEqual: ({ params }) => `Must be at most ${String(params.max)}.`,
46
+ greaterThanOrEqual: ({ params }) => `Must be at least ${String(params.min)}.`,
47
+ positive: "Must be positive.",
48
+ negative: "Must be negative.",
49
+ nonNegative: "Must be non-negative.",
50
+ nonPositive: "Must be non-positive.",
51
+ min: ({ params }) => `Must be ${params.inclusive === false ? "greater than" : "at least"} ${String(params.min)}.`,
52
+ max: ({ params }) => `Must be ${params.inclusive === false ? "less than" : "at most"} ${String(params.max)}.`,
53
+ between: ({ params }) => `Must be ${params.minInclusive === false ? "greater than" : "at least"} ${String(params.min)} and ${params.maxInclusive === false ? "less than" : "at most"} ${String(params.max)}.`,
54
+ multipleOf: ({ params }) => `Must be a multiple of ${String(params.divisor)}.`,
55
+ decimal: "Must be a decimal within the allowed number of fractional places.",
56
+ precisionScale: "Must satisfy the required precision and scale.",
57
+ equal: "Must equal the expected value.",
58
+ notEqual: "Must differ from the disallowed value.",
59
+ notNull: "Must not be null.",
60
+ required: "This value is required.",
61
+ notEmpty: "Must not be empty.",
62
+ empty: "Must be empty.",
63
+ unknown: "Invalid value.",
64
+ never: "No value is allowed.",
65
+ null: "Must be null.",
66
+ undefined: "Must be undefined.",
67
+ nan: "Must be NaN.",
68
+ instanceOf: "Must be an instance of the required class.",
69
+ file: "Must be a file satisfying the size, type and extension requirements.",
70
+ blob: "Must be a Blob or File satisfying the configured requirements.",
71
+ before: "Must be before the specified date.",
72
+ after: "Must be after the specified date.",
73
+ same: "Must equal the specified date.",
74
+ isPast: "Must be in the past.",
75
+ isFuture: "Must be in the future.",
76
+ length: ({ params }) => `Length must be ${String(params.length)}.`,
77
+ minLength: ({ params }) => `Length must be at least ${String(params.minLength)}.`,
78
+ maxLength: ({ params }) => `Length must be at most ${String(params.maxLength)}.`,
79
+ matches: "Must match the required pattern.",
80
+ includes: "Must contain the required substring.",
81
+ startsWith: "Must start with the required prefix.",
82
+ endsWith: "Must end with the required suffix.",
83
+ lowercase: "Must be lowercase.",
84
+ uppercase: "Must be uppercase.",
85
+ email: "Must be a valid email address.",
86
+ uuid: "Must be a valid UUID.",
87
+ guid: "Must be a valid GUID.",
88
+ url: "Must be a valid absolute URL.",
89
+ httpUrl: "Must be a valid HTTP or HTTPS URL.",
90
+ hostname: "Must be a valid hostname.",
91
+ e164: "Must be a phone number in E.164 format.",
92
+ emoji: "Must contain valid emoji sequences.",
93
+ base64: "Must be valid Base64.",
94
+ base64url: "Must be valid Base64url.",
95
+ hex: "Must be a hexadecimal string.",
96
+ jwt: "Must have valid JWT syntax.",
97
+ nanoid: "Must be a valid Nano ID.",
98
+ cuid: "Must be a valid CUID.",
99
+ cuid2: "Must be a valid CUID2.",
100
+ ulid: "Must be a valid ULID.",
101
+ xid: "Must be a valid XID.",
102
+ ksuid: "Must be a valid KSUID.",
103
+ ip: "Must be a valid IP address.",
104
+ ipv4: "Must be a valid IPv4 address.",
105
+ ipv6: "Must be a valid IPv6 address.",
106
+ cidr: "Must be valid CIDR notation.",
107
+ cidrv4: "Must be valid IPv4 CIDR notation.",
108
+ cidrv6: "Must be valid IPv6 CIDR notation.",
109
+ mac: "Must be a valid MAC address.",
110
+ creditCard: "Must be a valid card number.",
111
+ iban: "Must be a valid IBAN.",
112
+ currencyCode: "Must be a supported currency code.",
113
+ hash: "Must be a hash in the required format.",
114
+ isoDate: "Must be a valid ISO date.",
115
+ isoTime: "Must be a valid ISO time.",
116
+ isoDateTime: "Must be a valid ISO date and time.",
117
+ isoDuration: "Must be a valid ISO duration.",
118
+ });
@@ -0,0 +1,3 @@
1
+ import type { ValidationMessages } from "../Types.js";
2
+ /** Flat, dependency-free Russian catalog keyed by validation code. */
3
+ export declare const validationMessagesRu: ValidationMessages;