exisjs 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/config/config.d.ts.map +1 -1
  2. package/dist/config/config.js +2 -1
  3. package/dist/config/config.js.map +1 -1
  4. package/dist/config/types.d.ts +5 -0
  5. package/dist/config/types.d.ts.map +1 -1
  6. package/dist/database/lifecycle.d.ts +19 -2
  7. package/dist/database/lifecycle.d.ts.map +1 -1
  8. package/dist/database/lifecycle.js +92 -53
  9. package/dist/database/lifecycle.js.map +1 -1
  10. package/dist/decorators/transaction.d.ts +0 -21
  11. package/dist/decorators/transaction.d.ts.map +1 -1
  12. package/dist/decorators/transaction.js +52 -32
  13. package/dist/decorators/transaction.js.map +1 -1
  14. package/dist/docs/content/boundaries.mdx +2 -0
  15. package/dist/docs/content/config.mdx +4 -2
  16. package/dist/docs/content/decorators.mdx +1 -1
  17. package/dist/docs/content/middleware.mdx +4 -2
  18. package/dist/docs/content/requests.mdx +2 -0
  19. package/dist/docs/content/responses.mdx +54 -0
  20. package/dist/docs/content/validation.mdx +46 -11
  21. package/dist/docs/reference/README.md +16 -16
  22. package/dist/docs/reference/config.mdx +2 -1
  23. package/dist/docs/reference/database.mdx +3 -2
  24. package/dist/docs/reference/middleware.mdx +1 -1
  25. package/dist/docs/reference/sanitize.mdx +129 -134
  26. package/dist/docs/reference/validator.mdx +11 -3
  27. package/dist/middleware/compression.d.ts.map +1 -1
  28. package/dist/middleware/compression.js +6 -5
  29. package/dist/middleware/compression.js.map +1 -1
  30. package/dist/middleware/middleware.d.ts.map +1 -1
  31. package/dist/middleware/middleware.js +47 -34
  32. package/dist/middleware/middleware.js.map +1 -1
  33. package/dist/router/controller-registrar.d.ts.map +1 -1
  34. package/dist/router/controller-registrar.js +4 -7
  35. package/dist/router/controller-registrar.js.map +1 -1
  36. package/dist/router/route-scanner.d.ts +12 -4
  37. package/dist/router/route-scanner.d.ts.map +1 -1
  38. package/dist/router/route-scanner.js +46 -31
  39. package/dist/router/route-scanner.js.map +1 -1
  40. package/dist/router/router.d.ts +3 -0
  41. package/dist/router/router.d.ts.map +1 -1
  42. package/dist/router/router.js +40 -35
  43. package/dist/router/router.js.map +1 -1
  44. package/dist/router/scanner/boundary-loader.d.ts.map +1 -1
  45. package/dist/router/scanner/boundary-loader.js +25 -30
  46. package/dist/router/scanner/boundary-loader.js.map +1 -1
  47. package/dist/router/scanner/functional-compiler.d.ts.map +1 -1
  48. package/dist/router/scanner/functional-compiler.js +29 -29
  49. package/dist/router/scanner/functional-compiler.js.map +1 -1
  50. package/dist/sanitize/index.d.ts +29 -3
  51. package/dist/sanitize/index.d.ts.map +1 -1
  52. package/dist/sanitize/index.js +91 -32
  53. package/dist/sanitize/index.js.map +1 -1
  54. package/dist/server/adapters/bun-adapter.d.ts +1 -1
  55. package/dist/server/adapters/bun-adapter.d.ts.map +1 -1
  56. package/dist/server/adapters/bun-adapter.js.map +1 -1
  57. package/dist/server/app.d.ts.map +1 -1
  58. package/dist/server/app.js +5 -2
  59. package/dist/server/app.js.map +1 -1
  60. package/dist/server/bootstrapper.d.ts.map +1 -1
  61. package/dist/server/bootstrapper.js +1 -1
  62. package/dist/server/bootstrapper.js.map +1 -1
  63. package/dist/server/helpers/body-parser.d.ts +1 -1
  64. package/dist/server/helpers/body-parser.d.ts.map +1 -1
  65. package/dist/server/helpers/body-parser.js +9 -7
  66. package/dist/server/helpers/body-parser.js.map +1 -1
  67. package/dist/server/helpers/conditional.d.ts +60 -0
  68. package/dist/server/helpers/conditional.d.ts.map +1 -0
  69. package/dist/server/helpers/conditional.js +189 -0
  70. package/dist/server/helpers/conditional.js.map +1 -0
  71. package/dist/server/helpers/etag.d.ts +0 -1
  72. package/dist/server/helpers/etag.d.ts.map +1 -1
  73. package/dist/server/helpers/etag.js +0 -10
  74. package/dist/server/helpers/etag.js.map +1 -1
  75. package/dist/server/helpers/index.d.ts +2 -1
  76. package/dist/server/helpers/index.d.ts.map +1 -1
  77. package/dist/server/helpers/index.js +16 -2
  78. package/dist/server/helpers/index.js.map +1 -1
  79. package/dist/server/request-handler.d.ts.map +1 -1
  80. package/dist/server/request-handler.js +12 -2
  81. package/dist/server/request-handler.js.map +1 -1
  82. package/dist/server/request.d.ts +11 -3
  83. package/dist/server/request.d.ts.map +1 -1
  84. package/dist/server/request.js +41 -83
  85. package/dist/server/request.js.map +1 -1
  86. package/dist/server/response.d.ts +43 -2
  87. package/dist/server/response.d.ts.map +1 -1
  88. package/dist/server/response.js +151 -70
  89. package/dist/server/response.js.map +1 -1
  90. package/dist/validator/builder.d.ts +12 -1
  91. package/dist/validator/builder.d.ts.map +1 -1
  92. package/dist/validator/builder.js +74 -41
  93. package/dist/validator/builder.js.map +1 -1
  94. package/dist/validator/compile.d.ts +7 -2
  95. package/dist/validator/compile.d.ts.map +1 -1
  96. package/dist/validator/compile.js +85 -54
  97. package/dist/validator/compile.js.map +1 -1
  98. package/dist/validator/engine.d.ts +75 -6
  99. package/dist/validator/engine.d.ts.map +1 -1
  100. package/dist/validator/engine.js +483 -540
  101. package/dist/validator/engine.js.map +1 -1
  102. package/dist/validator/error.d.ts.map +1 -1
  103. package/dist/validator/error.js +7 -0
  104. package/dist/validator/error.js.map +1 -1
  105. package/dist/validator/rules.d.ts +66 -7
  106. package/dist/validator/rules.d.ts.map +1 -1
  107. package/dist/validator/rules.js +435 -109
  108. package/dist/validator/rules.js.map +1 -1
  109. package/dist/validator/tex-types.d.ts +15 -0
  110. package/dist/validator/tex-types.d.ts.map +1 -1
  111. package/dist/validator/tex-types.js +127 -64
  112. package/dist/validator/tex-types.js.map +1 -1
  113. package/dist/validator/text.d.ts +2 -0
  114. package/dist/validator/text.d.ts.map +1 -1
  115. package/dist/validator/text.js +31 -13
  116. package/dist/validator/text.js.map +1 -1
  117. package/dist/validator/types.d.ts +15 -0
  118. package/dist/validator/types.d.ts.map +1 -1
  119. package/package.json +3 -5
  120. package/dist/sanitize/sanitize.d.ts +0 -33
  121. package/dist/sanitize/sanitize.d.ts.map +0 -1
  122. package/dist/sanitize/sanitize.js +0 -104
  123. package/dist/sanitize/sanitize.js.map +0 -1
@@ -4,28 +4,64 @@
4
4
  // A rule string such as "string? | min:3 | trim" is parsed once, when the
5
5
  // schema is built, into a tree of small check functions. Validating a request
6
6
  // is then a plain walk over the data with no parsing and no allocation beyond
7
- // the result object. Error messages keep the established wording
8
- // ("Missing required field: x", "Field 'x' must be ...") which TexEngine maps
9
- // to structured error codes.
7
+ // the result object.
8
+ //
9
+ // The rule string describes the type and its modifiers. Everything a string
10
+ // cannot hold (sanitizer and refinement functions, default values, nested
11
+ // schemas' own options) comes from the schema object the rule was built from,
12
+ // passed alongside as `raw`, so nested fields behave exactly like top-level
13
+ // ones.
10
14
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.EMAIL = exports.RuleError = void 0;
12
- exports.toLegacyError = toLegacyError;
15
+ exports.EMAIL = exports.PATTERN_FAILED = exports.RuleError = void 0;
16
+ exports.formatPath = formatPath;
17
+ exports.prefixErrors = prefixErrors;
18
+ exports.ruleParts = ruleParts;
19
+ exports.ruleBase = ruleBase;
20
+ exports.parseEnumValues = parseEnumValues;
21
+ exports.formatEnumValues = formatEnumValues;
22
+ exports.parseLiteral = parseLiteral;
23
+ exports.refinementMessage = refinementMessage;
13
24
  exports.compileRule = compileRule;
25
+ exports.resolveDefault = resolveDefault;
26
+ exports.compileFields = compileFields;
14
27
  exports.compileObject = compileObject;
15
28
  exports.compileSchema = compileSchema;
29
+ exports.coerceBoolean = coerceBoolean;
30
+ exports.enumLabel = enumLabel;
16
31
  exports.parseJsonString = parseJsonString;
17
32
  exports.normalizeArrayInput = normalizeArrayInput;
33
+ exports.coerceArrayInput = coerceArrayInput;
18
34
  exports.dedupeItems = dedupeItems;
19
35
  const text_1 = require("./text");
36
+ const error_1 = require("./error");
20
37
  // Thrown by checks. The failing field's path is collected while the error
21
- // unwinds through containers, so valid input never builds path strings.
22
- class RuleError extends Error {
23
- constructor(kind, detail) {
24
- super(detail);
38
+ // unwinds through containers, so valid input never builds path strings. It
39
+ // is not an Error: a failed request should not pay for a stack trace.
40
+ class RuleError {
41
+ constructor(kind, detail,
42
+ /** Error code, when the constraint text alone does not determine it */
43
+ code, expected) {
25
44
  this.kind = kind;
26
45
  this.detail = detail;
46
+ this.code = code;
47
+ this.expected = expected;
27
48
  this.path = [];
28
49
  }
50
+ /** The established message format ("Field 'x' must be ...") */
51
+ get message() {
52
+ const path = formatPath(this.path);
53
+ switch (this.kind) {
54
+ case 'missing':
55
+ return `Missing required field: ${path}`;
56
+ case 'strict':
57
+ return `Strict mode error: Unknown field '${this.detail}' at path '${path}'`;
58
+ case 'input':
59
+ case 'custom':
60
+ return this.detail;
61
+ default:
62
+ return `Field '${path}' ${this.detail}`;
63
+ }
64
+ }
29
65
  }
30
66
  exports.RuleError = RuleError;
31
67
  function formatPath(segments) {
@@ -38,23 +74,26 @@ function formatPath(segments) {
38
74
  }
39
75
  return out;
40
76
  }
41
- // Keeps the established message formats that TexEngine maps to error codes
42
- function toLegacyError(err) {
43
- const path = formatPath(err.path);
44
- switch (err.kind) {
45
- case 'missing':
46
- return new Error(`Missing required field: ${path}`);
47
- case 'strict':
48
- return new Error(`Strict mode error: Unknown field '${err.detail}' at path '${path}'`);
49
- case 'input':
50
- return new Error(err.detail);
51
- default:
52
- return new Error(`Field '${path}' ${err.detail}`);
77
+ /** Puts `segments` in front of every path in a nested schema's errors. */
78
+ function prefixErrors(err, segments) {
79
+ const prefix = formatPath(segments);
80
+ if (prefix === '')
81
+ return;
82
+ for (const e of err.errors) {
83
+ const p = e.path;
84
+ e.path =
85
+ !p || p === 'root'
86
+ ? prefix
87
+ : p.startsWith('[')
88
+ ? prefix + p
89
+ : `${prefix}.${p}`;
53
90
  }
54
91
  }
55
92
  function prefixPath(err, segment) {
56
93
  if (err instanceof RuleError)
57
94
  err.path.unshift(segment);
95
+ else if (err instanceof error_1.ValidatorError)
96
+ prefixErrors(err, [segment]);
58
97
  throw err;
59
98
  }
60
99
  // ─── Rule string parsing ─────────────────────────────────────────────────────
@@ -103,8 +142,69 @@ function splitTopLevel(s, sep) {
103
142
  parts.push(tail);
104
143
  return parts;
105
144
  }
145
+ /** A rule string split into its type and modifiers, nesting kept intact */
146
+ function ruleParts(rule) {
147
+ return splitTopLevel(rule, '|');
148
+ }
149
+ /** The type part of a rule string: "string? | min:2" -> "string" */
150
+ function ruleBase(rule) {
151
+ const base = splitTopLevel(rule, '|')[0];
152
+ return base.endsWith('?') ? base.slice(0, -1).trim() : base;
153
+ }
154
+ /** Values of an `enum:` rule: JSON when a value holds `,` `|` or spaces at an end. */
155
+ function parseEnumValues(spec) {
156
+ if (spec.startsWith('[')) {
157
+ try {
158
+ const values = JSON.parse(spec);
159
+ if (Array.isArray(values))
160
+ return values.map(String);
161
+ }
162
+ catch {
163
+ // fall through to the plain list
164
+ }
165
+ }
166
+ return spec.split(',').map((v) => v.trim());
167
+ }
168
+ /** Encodes enum values for a rule string. */
169
+ function formatEnumValues(values) {
170
+ const plain = values.every((v) => v !== '' && v === v.trim() && !/[,|"<>{}()[\]]/.test(v));
171
+ return plain ? values.join(',') : JSON.stringify(values);
172
+ }
173
+ /**
174
+ * The value of a `literal:` rule. The builder writes JSON, so `literal:42`
175
+ * is the number and `literal:"42"` the string; unquoted text that is not a
176
+ * number or boolean is a string, as in hand-written rules.
177
+ */
178
+ function parseLiteral(spec) {
179
+ if (spec.startsWith('"')) {
180
+ try {
181
+ return JSON.parse(spec);
182
+ }
183
+ catch {
184
+ return spec;
185
+ }
186
+ }
187
+ if (spec === 'true')
188
+ return true;
189
+ if (spec === 'false')
190
+ return false;
191
+ if (spec.trim() !== '' && Number.isFinite(Number(spec)))
192
+ return Number(spec);
193
+ return spec;
194
+ }
195
+ function readPattern(arg) {
196
+ try {
197
+ const [source, flags] = JSON.parse(arg);
198
+ // g and y make test() stateful across calls
199
+ return new RegExp(source, String(flags ?? '').replace(/[gy]/g, ''));
200
+ }
201
+ catch {
202
+ return undefined;
203
+ }
204
+ }
106
205
  function readModifiers(parts, field) {
107
206
  const m = {
207
+ int: false,
108
208
  coerce: false,
109
209
  dedupe: false,
110
210
  trim: false,
@@ -128,7 +228,7 @@ function readModifiers(parts, field) {
128
228
  if (colon !== -1) {
129
229
  const name = p.slice(0, colon);
130
230
  const arg = p.slice(colon + 1);
131
- if (name === 'min' || name === 'max') {
231
+ if (name === 'min' || name === 'max' || name === 'length') {
132
232
  const n = Number(arg);
133
233
  if (arg.trim() !== '' && Number.isFinite(n))
134
234
  m[name] = n;
@@ -140,8 +240,14 @@ function readModifiers(parts, field) {
140
240
  if (Number.isInteger(n))
141
241
  m.version = n;
142
242
  }
143
- // minDate/maxDate/maxSize/mimeTypes are enforced by TexEngine and the
144
- // upload middleware
243
+ else if (name === 'minDate' || name === 'maxDate')
244
+ m[name] = arg;
245
+ else if (name === 'pattern')
246
+ m.pattern = readPattern(arg);
247
+ else if (name === 'protocols') {
248
+ m.protocols = arg.split(',').map((s) => s.trim().toLowerCase());
249
+ }
250
+ // maxSize/mimeTypes are enforced by the upload middleware
145
251
  continue;
146
252
  }
147
253
  switch (p) {
@@ -155,6 +261,7 @@ function readModifiers(parts, field) {
155
261
  field.optional = true;
156
262
  field.nullable = true;
157
263
  break;
264
+ case 'int':
158
265
  case 'coerce':
159
266
  case 'dedupe':
160
267
  case 'trim':
@@ -177,10 +284,99 @@ function readModifiers(parts, field) {
177
284
  }
178
285
  return m;
179
286
  }
180
- function compileRule(rule) {
287
+ // ─── Schema objects next to the rule string ──────────────────────────────────
288
+ // Duck-typed so this file does not import the builder classes.
289
+ function isTexType(raw) {
290
+ return (raw != null && typeof raw._raw === 'string' && Array.isArray(raw.sanitizers));
291
+ }
292
+ function isEngine(raw) {
293
+ return raw != null && typeof raw.getCompiledSchema === 'function';
294
+ }
295
+ /** The object schema behind `raw`: an engine, or the inner engine of `.transform()` */
296
+ function engineOf(raw) {
297
+ if (isEngine(raw))
298
+ return raw;
299
+ if (raw != null &&
300
+ isEngine(raw.inner) &&
301
+ typeof raw.transformFn === 'function') {
302
+ return raw.inner;
303
+ }
304
+ return undefined;
305
+ }
306
+ function isDiscriminatedUnion(raw) {
307
+ return (raw != null &&
308
+ typeof raw.discriminator === 'string' &&
309
+ typeof raw.parse === 'function');
310
+ }
311
+ function sanitizerCheck(fns) {
312
+ return (v) => {
313
+ for (const fn of fns) {
314
+ if (v === undefined || v === null)
315
+ return v;
316
+ try {
317
+ v = fn(v);
318
+ }
319
+ catch (err) {
320
+ throw new RuleError('custom', err?.message || 'Sanitization failed', 'SANITIZATION_FAILED', 'a value the sanitizer accepts');
321
+ }
322
+ }
323
+ return v;
324
+ };
325
+ }
326
+ function refinementMessage(r, value) {
327
+ return typeof r.message === 'function'
328
+ ? r.message(value)
329
+ : r.message || 'Invalid value';
330
+ }
331
+ function postCheck(refinements, transforms) {
332
+ return (v) => {
333
+ for (const r of refinements) {
334
+ if (!r.fn(v)) {
335
+ throw new RuleError('custom', refinementMessage(r, v), 'CUSTOM_VALIDATION', 'custom refinement rule');
336
+ }
337
+ }
338
+ for (const t of transforms)
339
+ v = t(v);
340
+ return v;
341
+ };
342
+ }
343
+ /** Adds the functions and default of the schema object to a compiled rule. */
344
+ function attachRaw(field, raw) {
345
+ if (isTexType(raw)) {
346
+ if (raw.sanitizers.length > 0)
347
+ field.pre = sanitizerCheck(raw.sanitizers);
348
+ const sync = raw.refinements.filter((r) => !r.async);
349
+ if (sync.length > 0 || raw.transformations.length > 0) {
350
+ field.post = postCheck(sync, raw.transformations);
351
+ }
352
+ if (raw.defaultValue !== undefined) {
353
+ field.defaultRef = { value: raw.defaultValue };
354
+ }
355
+ }
356
+ else {
357
+ const objectCheck = engineOf(raw)?._objectCheck;
358
+ const transform = raw?.transformFn;
359
+ if (objectCheck && transform)
360
+ field.post = (v) => transform(objectCheck(v));
361
+ else if (objectCheck || transform)
362
+ field.post = objectCheck ?? transform;
363
+ }
364
+ const { pre, base, post } = field;
365
+ field.check =
366
+ pre && post
367
+ ? (v) => post(base(pre(v)))
368
+ : pre
369
+ ? (v) => base(pre(v))
370
+ : post
371
+ ? (v) => post(base(v))
372
+ : base;
373
+ return field;
374
+ }
375
+ function compileRule(rule, raw) {
181
376
  const field = {
182
377
  optional: false,
183
378
  nullable: false,
379
+ base: passThrough,
184
380
  check: passThrough,
185
381
  gen: { kind: 'pass' },
186
382
  };
@@ -188,12 +384,13 @@ function compileRule(rule) {
188
384
  // modifiers) using "||", so "number | coerce || email" is two rules
189
385
  const alternatives = splitTopLevel(rule, '||');
190
386
  if (alternatives.length > 1) {
191
- const rules = alternatives.map(compileRule);
387
+ const options = raw?._options;
388
+ const rules = alternatives.map((a, i) => compileRule(a, options?.[i]));
192
389
  field.optional = rules.some((r) => r.optional);
193
390
  field.nullable = rules.some((r) => r.nullable);
194
- field.check = unionCheck(rules);
391
+ field.base = unionCheck(rules);
195
392
  field.gen = undefined;
196
- return field;
393
+ return attachRaw(field, raw);
197
394
  }
198
395
  const parts = splitTopLevel(rule, '|');
199
396
  let base = parts[0];
@@ -205,84 +402,122 @@ function compileRule(rule) {
205
402
  field.defaultValue = m.defaultValue;
206
403
  // "(a || b)?" groups a whole rule; compile the inside and keep its flags
207
404
  if (base.startsWith('(') && base.endsWith(')')) {
208
- const inner = compileRule(base.slice(1, -1));
209
- field.check = inner.check;
405
+ const inner = compileRule(base.slice(1, -1), { _options: raw?._options });
406
+ field.base = inner.check;
210
407
  field.gen = inner.gen;
211
408
  field.optional = field.optional || inner.optional;
212
409
  field.nullable = field.nullable || inner.nullable;
213
- return field;
410
+ return attachRaw(field, raw);
411
+ }
412
+ if (isDiscriminatedUnion(raw)) {
413
+ field.base = (v) => raw.parse(parseJsonString(v, '{', '}'));
414
+ field.gen = undefined;
214
415
  }
215
- if (base.startsWith('object<') && base.endsWith('>')) {
216
- const inner = parseObjectSchema(base.slice(7, -1));
217
- field.check = objectCheck(inner);
218
- if (inner)
219
- field.gen = { kind: 'object', schema: inner };
416
+ else if (base.startsWith('object<') && base.endsWith('>')) {
417
+ const engine = engineOf(raw);
418
+ const schema = engine
419
+ ? engine.getCompiledSchema()
420
+ : parseObjectSchema(base.slice(7, -1));
421
+ const strict = engine ? Boolean(engine.strict) : false;
422
+ const rawSchema = engine?.rawSchema;
423
+ field.base = objectCheck(schema, strict, rawSchema);
424
+ field.gen = schema
425
+ ? { kind: 'object', schema, raw: rawSchema, strict }
426
+ : { kind: 'pass' };
220
427
  }
221
428
  else if (base.startsWith('array<') && base.endsWith('>')) {
222
- const item = compileRule(base.slice(6, -1));
223
- field.check = arrayCheck(item, m);
429
+ let inner = base.slice(6, -1);
430
+ // Comma-split query values are strings; their items convert with the array
431
+ if (m.coerce && /^(number|boolean)\b/.test(inner))
432
+ inner += ' | coerce';
433
+ const item = compileRule(inner, raw?._item);
434
+ field.base = arrayCheck(item, m);
224
435
  field.gen = { kind: 'array', item, m };
225
436
  }
226
437
  else if (base.startsWith('record<') && base.endsWith('>')) {
227
- field.check = recordCheck(compileRule(base.slice(7, -1)));
438
+ field.base = recordCheck(compileRule(base.slice(7, -1), raw?._value));
228
439
  field.gen = undefined;
229
440
  }
230
441
  else if (base.startsWith('enum:')) {
231
- const values = base
232
- .slice(5)
233
- .split(',')
234
- .map((v) => v.trim());
235
- field.check = enumCheck(values);
442
+ const values = parseEnumValues(base.slice(5));
443
+ field.base = enumCheck(values);
236
444
  field.gen = { kind: 'enum', values };
237
445
  }
238
446
  else if (base.startsWith('literal:')) {
239
- field.check = literalCheck(base.slice(8));
447
+ field.base = literalCheck(parseLiteral(base.slice(8)));
240
448
  field.gen = undefined;
241
449
  }
242
450
  else {
451
+ field.gen = undefined;
243
452
  switch (base) {
244
453
  case 'string':
245
- field.check = stringCheck(m);
454
+ field.base = stringCheck(m);
246
455
  field.gen = { kind: 'string', m };
247
456
  break;
248
457
  case 'number':
249
- field.check = numberCheck(m);
458
+ field.base = numberCheck(m);
250
459
  field.gen = { kind: 'number', m };
251
460
  break;
252
461
  case 'boolean':
253
- field.check = booleanCheck(m);
462
+ field.base = booleanCheck(m);
254
463
  field.gen = { kind: 'boolean', m };
255
464
  break;
256
465
  case 'email':
257
- field.check = emailCheck(m);
466
+ field.base = emailCheck(m);
258
467
  field.gen = { kind: 'email', m };
259
468
  break;
469
+ case 'url':
470
+ field.base = urlCheck(m);
471
+ break;
472
+ case 'date':
473
+ field.base = dateCheck(m);
474
+ break;
260
475
  case 'password':
261
- field.check = passwordCheck(m);
262
- field.gen = undefined;
476
+ field.base = passwordCheck(m);
263
477
  break;
264
478
  case 'uuid':
265
- field.check = uuidCheck(m.version);
266
- field.gen = undefined;
479
+ field.base = uuidCheck(m.version);
267
480
  break;
268
481
  case 'cuid':
269
- field.check = patternCheck(CUID, 'must be a valid CUID');
270
- field.gen = undefined;
482
+ field.base = patternCheck(CUID, 'must be a valid CUID');
271
483
  break;
272
484
  case 'creditcard':
273
- field.check = creditCardCheck(m);
274
- field.gen = undefined;
485
+ field.base = creditCardCheck(m);
275
486
  break;
276
- // date is validated by TexEngine; file by the upload middleware
487
+ // any; file is validated by the upload middleware
277
488
  default:
278
- field.check = passThrough;
489
+ field.base = passThrough;
490
+ field.gen = { kind: 'pass' };
279
491
  }
280
492
  }
281
- return field;
493
+ return attachRaw(field, raw);
282
494
  }
283
- function compileObject(schema, strict) {
495
+ /** Default values that are objects are copied, so parses never share them */
496
+ function resolveDefault(value) {
497
+ if (typeof value === 'function')
498
+ return value();
499
+ if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
500
+ try {
501
+ return structuredClone(value);
502
+ }
503
+ catch {
504
+ return value;
505
+ }
506
+ }
507
+ return value;
508
+ }
509
+ // Query strings and path params are text; number and boolean fields convert
510
+ function withQueryCoercion(rule) {
511
+ return /^(number|boolean)\b/.test(rule) && !rule.includes('||')
512
+ ? `${rule} | coerce`
513
+ : rule;
514
+ }
515
+ function compileFields(schema, raw, query = false) {
516
+ return Object.keys(schema).map((k) => compileRule(query ? withQueryCoercion(schema[k]) : schema[k], raw?.[k]));
517
+ }
518
+ function compileObject(schema, strict, raw, query = false) {
284
519
  const keys = Object.keys(schema);
285
- const fields = keys.map((k) => compileRule(schema[k]));
520
+ const fields = compileFields(schema, raw, query);
286
521
  const known = new Set(keys);
287
522
  return (obj) => {
288
523
  if (strict) {
@@ -298,22 +533,32 @@ function compileObject(schema, strict) {
298
533
  const key = keys[i];
299
534
  const field = fields[i];
300
535
  let value = obj[key];
536
+ // A blank form field counts as left out (or null when only nullable)
537
+ if ((field.optional || field.nullable) &&
538
+ typeof value === 'string' &&
539
+ value.trim() === '') {
540
+ value = field.nullable && !field.optional ? null : undefined;
541
+ }
542
+ if (value === undefined || (value === null && !field.nullable)) {
543
+ if (field.defaultRef)
544
+ value = resolveDefault(field.defaultRef.value);
545
+ else if (field.defaultValue !== undefined)
546
+ value = field.defaultValue;
547
+ }
548
+ if (field.pre && value !== undefined && value !== null) {
549
+ value = field.pre(value);
550
+ }
301
551
  if (value === undefined || value === null) {
302
552
  if (value === null && field.nullable) {
303
553
  out[key] = null;
304
554
  continue;
305
555
  }
306
- if (field.defaultValue !== undefined) {
307
- value = field.defaultValue;
308
- }
309
- else if (!field.optional) {
556
+ if (!field.optional)
310
557
  throw new RuleError('missing', '');
311
- }
312
- else {
313
- continue;
314
- }
558
+ continue;
315
559
  }
316
- out[key] = field.check(value);
560
+ value = field.base(value);
561
+ out[key] = field.post ? field.post(value) : value;
317
562
  }
318
563
  }
319
564
  catch (err) {
@@ -323,24 +568,19 @@ function compileObject(schema, strict) {
323
568
  };
324
569
  }
325
570
  /** Builds the top-level validator TexEngine calls for every parse */
326
- function compileSchema(schema, strict) {
327
- const validate = compileObject(schema, strict);
571
+ function compileSchema(schema, strict, raw, query = false) {
572
+ const validate = compileObject(schema, strict, raw, query);
328
573
  return (data) => {
329
574
  if (data === null || typeof data !== 'object' || Array.isArray(data)) {
330
575
  throw new RuleError('input', 'Input must be a JSON object');
331
576
  }
332
- try {
333
- return validate(data);
334
- }
335
- catch (err) {
336
- throw err instanceof RuleError ? toLegacyError(err) : err;
337
- }
577
+ return validate(data);
338
578
  };
339
579
  }
340
580
  // ─── Field checks ────────────────────────────────────────────────────────────
341
581
  const passThrough = (value) => value;
342
- function fail(constraint) {
343
- throw new RuleError('field', constraint);
582
+ function fail(constraint, code, expected) {
583
+ throw new RuleError('field', constraint, code, expected);
344
584
  }
345
585
  function nullableCheck(rule) {
346
586
  return rule.nullable ? (v) => (v === null ? null : rule.check(v)) : rule.check;
@@ -367,6 +607,21 @@ function applyTextModifiers(s, m) {
367
607
  }
368
608
  return s;
369
609
  }
610
+ exports.PATTERN_FAILED = 'must match the required format';
611
+ function checkLength(s, m) {
612
+ if (m.min !== undefined && s.length < m.min) {
613
+ fail(`must be at least ${m.min} characters`);
614
+ }
615
+ if (m.max !== undefined && s.length > m.max) {
616
+ fail(`must be at most ${m.max} characters`);
617
+ }
618
+ if (m.length !== undefined && s.length !== m.length) {
619
+ fail(`must be exactly ${m.length} characters`, 'INVALID_LENGTH');
620
+ }
621
+ if (m.pattern !== undefined && !m.pattern.test(s)) {
622
+ fail(exports.PATTERN_FAILED, 'INVALID_FORMAT', `match ${m.pattern}`);
623
+ }
624
+ }
370
625
  function stringCheck(m) {
371
626
  return (v) => {
372
627
  if (m.coerce && (typeof v === 'number' || typeof v === 'boolean')) {
@@ -374,16 +629,9 @@ function stringCheck(m) {
374
629
  }
375
630
  if (typeof v !== 'string')
376
631
  fail('must be a string');
377
- let s = applyTextModifiers(v, m);
378
- if (m.mask)
379
- s = (0, text_1.maskString)(s);
380
- if (m.min !== undefined && s.length < m.min) {
381
- fail(`must be at least ${m.min} characters`);
382
- }
383
- if (m.max !== undefined && s.length > m.max) {
384
- fail(`must be at most ${m.max} characters`);
385
- }
386
- return s;
632
+ const s = applyTextModifiers(v, m);
633
+ checkLength(s, m);
634
+ return m.mask ? (0, text_1.maskString)(s) : s;
387
635
  };
388
636
  }
389
637
  function numberCheck(m) {
@@ -396,6 +644,9 @@ function numberCheck(m) {
396
644
  if (typeof v !== 'number' || !Number.isFinite(v)) {
397
645
  fail('must be a number');
398
646
  }
647
+ if (m.int && !Number.isInteger(v)) {
648
+ fail('must be an integer', 'INVALID_TYPE', 'integer');
649
+ }
399
650
  if (m.min !== undefined && v < m.min)
400
651
  fail(`must be >= ${m.min}`);
401
652
  if (m.max !== undefined && v > m.max)
@@ -403,14 +654,26 @@ function numberCheck(m) {
403
654
  return v;
404
655
  };
405
656
  }
657
+ /** "true" / "1" / 1 and "false" / "0" / 0, ignoring case and surrounding spaces */
658
+ function coerceBoolean(v) {
659
+ if (typeof v === 'string') {
660
+ const s = v.trim().toLowerCase();
661
+ if (s === 'true' || s === '1')
662
+ return true;
663
+ if (s === 'false' || s === '0')
664
+ return false;
665
+ return v;
666
+ }
667
+ if (v === 1)
668
+ return true;
669
+ if (v === 0)
670
+ return false;
671
+ return v;
672
+ }
406
673
  function booleanCheck(m) {
407
674
  return (v) => {
408
- if (m.coerce) {
409
- if (v === 'true' || v === '1' || v === 1)
410
- v = true;
411
- else if (v === 'false' || v === '0' || v === 0)
412
- v = false;
413
- }
675
+ if (m.coerce)
676
+ v = coerceBoolean(v);
414
677
  if (typeof v !== 'boolean')
415
678
  fail('must be a boolean');
416
679
  return v;
@@ -433,6 +696,61 @@ function emailCheck(m) {
433
696
  return s;
434
697
  };
435
698
  }
699
+ function urlCheck(m) {
700
+ const protocols = new Set((m.protocols ?? ['http', 'https']).map((p) => `${p.replace(/:$/, '')}:`));
701
+ const allowed = [...protocols].map((p) => p.slice(0, -1)).join(', ');
702
+ return (v) => {
703
+ if (typeof v !== 'string')
704
+ fail('must be a string');
705
+ const s = m.trim ? v.trim() : v;
706
+ let url;
707
+ try {
708
+ url = new URL(s);
709
+ }
710
+ catch {
711
+ return fail('must be a valid URL', 'INVALID_URL', 'absolute URL');
712
+ }
713
+ if (!protocols.has(url.protocol)) {
714
+ fail(`must use ${allowed}`, 'INVALID_URL', `${allowed} URL`);
715
+ }
716
+ if ((url.protocol === 'http:' || url.protocol === 'https:') &&
717
+ !url.hostname) {
718
+ fail('must be a valid URL', 'INVALID_URL', 'absolute URL');
719
+ }
720
+ checkLength(s, m);
721
+ return s;
722
+ };
723
+ }
724
+ /**
725
+ * Accepts a Date, or a date string or timestamp, and always returns a Date
726
+ * (JSON has no date type, so a body always sends text). `coerce` is accepted
727
+ * for compatibility and changes nothing.
728
+ */
729
+ function dateCheck(m) {
730
+ const min = m.minDate !== undefined ? new Date(m.minDate).getTime() : NaN;
731
+ const max = m.maxDate !== undefined ? new Date(m.maxDate).getTime() : NaN;
732
+ return (v) => {
733
+ const isText = typeof v === 'string' || typeof v === 'number';
734
+ if (!isText && !(v instanceof Date)) {
735
+ fail('must be a valid date', 'INVALID_TYPE', 'valid date or date string');
736
+ }
737
+ const time = v instanceof Date
738
+ ? v.getTime()
739
+ : typeof v === 'string' && v.trim() === ''
740
+ ? NaN
741
+ : new Date(v).getTime();
742
+ if (Number.isNaN(time)) {
743
+ fail('must be a valid date', 'INVALID_DATE', 'valid date string or timestamp');
744
+ }
745
+ if (time < min) {
746
+ fail(`date must be after ${m.minDate}`, 'DATE_TOO_EARLY', `>= ${m.minDate}`);
747
+ }
748
+ if (time > max) {
749
+ fail(`date must be before ${m.maxDate}`, 'DATE_TOO_LATE', `<= ${m.maxDate}`);
750
+ }
751
+ return v instanceof Date ? v : new Date(time);
752
+ };
753
+ }
436
754
  function passwordCheck(m) {
437
755
  return (v) => {
438
756
  if (typeof v !== 'string')
@@ -458,9 +776,12 @@ function passwordCheck(m) {
458
776
  return v;
459
777
  };
460
778
  }
779
+ function enumLabel(values) {
780
+ return `[${values.map((v) => JSON.stringify(v)).join(', ')}]`;
781
+ }
461
782
  function enumCheck(values) {
462
783
  const allowed = new Set(values);
463
- const label = `[${values.map((v) => JSON.stringify(v)).join(', ')}]`;
784
+ const label = enumLabel(values);
464
785
  return (v) => {
465
786
  if (typeof v !== 'string')
466
787
  fail('must be a string');
@@ -469,16 +790,10 @@ function enumCheck(values) {
469
790
  return v;
470
791
  };
471
792
  }
472
- // A literal rule is text ("literal:42"), so compare the value's string form;
473
- // the value itself keeps its original type
474
793
  function literalCheck(literal) {
475
794
  return (v) => {
476
- if ((typeof v !== 'string' &&
477
- typeof v !== 'number' &&
478
- typeof v !== 'boolean') ||
479
- String(v) !== literal) {
795
+ if (v !== literal)
480
796
  fail(`must be exactly ${JSON.stringify(literal)}`);
481
- }
482
797
  return v;
483
798
  };
484
799
  }
@@ -556,6 +871,15 @@ function normalizeArrayInput(v) {
556
871
  return [v];
557
872
  return v;
558
873
  }
874
+ /** `coerce` arrays also accept "a,b,c" and a single value */
875
+ function coerceArrayInput(v) {
876
+ const normalized = normalizeArrayInput(v);
877
+ if (Array.isArray(normalized))
878
+ return normalized;
879
+ if (typeof v === 'string')
880
+ return v.split(',').map((s) => s.trim());
881
+ return [v];
882
+ }
559
883
  function dedupeItems(items) {
560
884
  const seen = new Set();
561
885
  return items.filter((x) => {
@@ -568,9 +892,10 @@ function dedupeItems(items) {
568
892
  }
569
893
  function arrayCheck(item, m) {
570
894
  const checkItem = nullableCheck(item);
895
+ const toArray = m.coerce ? coerceArrayInput : normalizeArrayInput;
571
896
  return (v) => {
572
897
  if (!Array.isArray(v))
573
- v = normalizeArrayInput(v);
898
+ v = toArray(v);
574
899
  if (!Array.isArray(v))
575
900
  fail('must be an array');
576
901
  if (m.min !== undefined && v.length < m.min) {
@@ -604,10 +929,10 @@ function parseObjectSchema(innerJson) {
604
929
  }
605
930
  return null;
606
931
  }
607
- function objectCheck(schema) {
932
+ function objectCheck(schema, strict, raw) {
608
933
  if (!schema)
609
934
  return passThrough;
610
- const validate = compileObject(schema, false);
935
+ const validate = compileObject(schema, strict, raw);
611
936
  return (v) => {
612
937
  v = parseJsonString(v, '{', '}');
613
938
  if (v === null || typeof v !== 'object' || Array.isArray(v)) {
@@ -645,8 +970,9 @@ function unionCheck(options) {
645
970
  return check(v);
646
971
  }
647
972
  catch (err) {
648
- if (!(err instanceof RuleError))
973
+ if (!(err instanceof RuleError) && !(err instanceof error_1.ValidatorError)) {
649
974
  throw err;
975
+ }
650
976
  }
651
977
  }
652
978
  return fail('must match one of the allowed types');