exisjs 0.9.1 → 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 (58) 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/docs/content/config.mdx +3 -1
  7. package/dist/docs/content/validation.mdx +33 -11
  8. package/dist/docs/reference/config.mdx +1 -0
  9. package/dist/docs/reference/sanitize.mdx +129 -134
  10. package/dist/docs/reference/validator.mdx +8 -3
  11. package/dist/router/route-scanner.d.ts +12 -4
  12. package/dist/router/route-scanner.d.ts.map +1 -1
  13. package/dist/router/route-scanner.js +46 -31
  14. package/dist/router/route-scanner.js.map +1 -1
  15. package/dist/sanitize/index.d.ts +29 -3
  16. package/dist/sanitize/index.d.ts.map +1 -1
  17. package/dist/sanitize/index.js +91 -32
  18. package/dist/sanitize/index.js.map +1 -1
  19. package/dist/server/adapters/bun-adapter.d.ts +1 -1
  20. package/dist/server/adapters/bun-adapter.d.ts.map +1 -1
  21. package/dist/server/adapters/bun-adapter.js.map +1 -1
  22. package/dist/server/bootstrapper.d.ts.map +1 -1
  23. package/dist/server/bootstrapper.js +1 -1
  24. package/dist/server/bootstrapper.js.map +1 -1
  25. package/dist/validator/builder.d.ts +12 -1
  26. package/dist/validator/builder.d.ts.map +1 -1
  27. package/dist/validator/builder.js +63 -24
  28. package/dist/validator/builder.js.map +1 -1
  29. package/dist/validator/compile.d.ts +7 -2
  30. package/dist/validator/compile.d.ts.map +1 -1
  31. package/dist/validator/compile.js +85 -54
  32. package/dist/validator/compile.js.map +1 -1
  33. package/dist/validator/engine.d.ts +58 -7
  34. package/dist/validator/engine.d.ts.map +1 -1
  35. package/dist/validator/engine.js +432 -525
  36. package/dist/validator/engine.js.map +1 -1
  37. package/dist/validator/error.d.ts.map +1 -1
  38. package/dist/validator/error.js +7 -0
  39. package/dist/validator/error.js.map +1 -1
  40. package/dist/validator/rules.d.ts +66 -7
  41. package/dist/validator/rules.d.ts.map +1 -1
  42. package/dist/validator/rules.js +435 -109
  43. package/dist/validator/rules.js.map +1 -1
  44. package/dist/validator/tex-types.d.ts +15 -0
  45. package/dist/validator/tex-types.d.ts.map +1 -1
  46. package/dist/validator/tex-types.js +127 -64
  47. package/dist/validator/tex-types.js.map +1 -1
  48. package/dist/validator/text.d.ts +2 -0
  49. package/dist/validator/text.d.ts.map +1 -1
  50. package/dist/validator/text.js +31 -13
  51. package/dist/validator/text.js.map +1 -1
  52. package/dist/validator/types.d.ts +15 -0
  53. package/dist/validator/types.d.ts.map +1 -1
  54. package/package.json +1 -1
  55. package/dist/sanitize/sanitize.d.ts +0 -33
  56. package/dist/sanitize/sanitize.d.ts.map +0 -1
  57. package/dist/sanitize/sanitize.js +0 -104
  58. package/dist/sanitize/sanitize.js.map +0 -1
@@ -5,19 +5,47 @@ exports.fieldRule = fieldRule;
5
5
  const compile_1 = require("./compile");
6
6
  const tex_types_1 = require("./tex-types");
7
7
  const error_1 = require("./error");
8
+ const rules_1 = require("./rules");
9
+ function safely(run) {
10
+ try {
11
+ return { success: true, data: run() };
12
+ }
13
+ catch (err) {
14
+ if (err instanceof error_1.ValidatorError) {
15
+ return { success: false, errors: err.errors, error: err };
16
+ }
17
+ throw err;
18
+ }
19
+ }
20
+ async function safelyAsync(run) {
21
+ try {
22
+ return { success: true, data: await run() };
23
+ }
24
+ catch (err) {
25
+ if (err instanceof error_1.ValidatorError) {
26
+ return { success: false, errors: err.errors, error: err };
27
+ }
28
+ throw err;
29
+ }
30
+ }
8
31
  function engineModifiers(engine) {
9
32
  return ((engine._isOptional ? ' | optional' : '') +
10
33
  (engine._isNullable ? ' | nullable' : ''));
11
34
  }
12
35
  /**
13
36
  * The rule string for one field of an object schema. Shared by `tex.object`,
14
- * `tex.array` and `extend` so a nested schema keeps its `optional()` /
15
- * `nullable()` modifiers on every path.
37
+ * `tex.array`, `tex.record`, `tex.union` and `extend` so a nested schema
38
+ * keeps its `optional()` / `nullable()` modifiers on every path.
16
39
  */
17
40
  function fieldRule(key, value) {
18
41
  if (value instanceof TexEngine) {
19
42
  return `object<${JSON.stringify(value.getCompiledSchema())}>${engineModifiers(value)}`;
20
43
  }
44
+ if (value instanceof TexTransformedEngine) {
45
+ return fieldRule(key, value.inner);
46
+ }
47
+ if (value instanceof TexDiscriminatedUnionEngine)
48
+ return 'object';
21
49
  if (value instanceof tex_types_1.TexType)
22
50
  return value._raw;
23
51
  if (typeof value === 'string')
@@ -27,34 +55,169 @@ function fieldRule(key, value) {
27
55
  }
28
56
  throw new TypeError(`tex.object(): field "${key}" is not a field schema (got ${value?.constructor?.name ?? typeof value}); use tex.object(), tex.array() or a tex.* field`);
29
57
  }
58
+ // Error code and expected value for a constraint text ("must be a string")
59
+ function describeConstraint(constraint) {
60
+ if (constraint.startsWith('must be a string')) {
61
+ return { code: 'INVALID_TYPE', expected: 'string' };
62
+ }
63
+ if (constraint.startsWith('must be a number')) {
64
+ return { code: 'INVALID_TYPE', expected: 'number' };
65
+ }
66
+ if (constraint.startsWith('must be a boolean')) {
67
+ return { code: 'INVALID_TYPE', expected: 'boolean' };
68
+ }
69
+ if (constraint.startsWith('must be an array')) {
70
+ return { code: 'INVALID_TYPE', expected: 'array' };
71
+ }
72
+ if (constraint.includes('array must have')) {
73
+ return { code: 'ARRAY_TOO_SHORT', expected: 'array' };
74
+ }
75
+ if (constraint.includes('array exceeds')) {
76
+ return { code: 'ARRAY_TOO_LONG', expected: 'array' };
77
+ }
78
+ if (constraint.startsWith('must be an object')) {
79
+ return { code: 'INVALID_TYPE', expected: 'object' };
80
+ }
81
+ if (constraint.startsWith('must be a valid email')) {
82
+ return { code: 'INVALID_EMAIL', expected: 'valid email address' };
83
+ }
84
+ if (constraint.startsWith('must be one of')) {
85
+ return {
86
+ code: 'INVALID_ENUM_VALUE',
87
+ expected: constraint.replace('must be one of', '').trim(),
88
+ };
89
+ }
90
+ if (constraint.startsWith('must be at least') ||
91
+ constraint.startsWith('must be >=')) {
92
+ return { code: 'VALUE_TOO_SMALL', expected: constraint };
93
+ }
94
+ if (constraint.startsWith('must be at most') ||
95
+ constraint.startsWith('must be <=')) {
96
+ return { code: 'VALUE_TOO_LARGE', expected: constraint };
97
+ }
98
+ if (constraint.startsWith('must contain a number')) {
99
+ return {
100
+ code: 'PASSWORD_REQUIRES_NUMBER',
101
+ expected: 'must contain at least one digit',
102
+ };
103
+ }
104
+ if (constraint.startsWith('must contain an uppercase')) {
105
+ return {
106
+ code: 'PASSWORD_REQUIRES_UPPERCASE',
107
+ expected: 'must contain at least one uppercase letter',
108
+ };
109
+ }
110
+ if (constraint.startsWith('must contain a lowercase')) {
111
+ return {
112
+ code: 'PASSWORD_REQUIRES_LOWERCASE',
113
+ expected: 'must contain at least one lowercase letter',
114
+ };
115
+ }
116
+ if (constraint.startsWith('must contain a symbol')) {
117
+ return {
118
+ code: 'PASSWORD_REQUIRES_SYMBOL',
119
+ expected: 'must contain at least one symbol',
120
+ };
121
+ }
122
+ return { code: 'VALIDATION_ERROR', expected: constraint };
123
+ }
124
+ /** Turns a failed rule into a structured error entry */
125
+ function describeRuleError(err, data) {
126
+ let path = (0, rules_1.formatPath)(err.path);
127
+ let message;
128
+ let expected;
129
+ let code;
130
+ switch (err.kind) {
131
+ case 'missing':
132
+ message = 'Expected value, received undefined';
133
+ expected = 'defined';
134
+ code = 'MISSING_REQUIRED_FIELD';
135
+ break;
136
+ case 'strict':
137
+ path = path ? `${path}.${err.detail}` : err.detail;
138
+ message = 'Unknown field not allowed in strict mode';
139
+ expected = 'undefined (field omitted)';
140
+ code = 'UNRECOGNIZED_KEYS';
141
+ break;
142
+ case 'input':
143
+ message = err.detail;
144
+ code = 'VALIDATION_ERROR';
145
+ break;
146
+ case 'custom':
147
+ message = err.detail;
148
+ expected = err.expected;
149
+ code = err.code ?? 'CUSTOM_VALIDATION';
150
+ break;
151
+ default: {
152
+ message = err.detail.charAt(0).toUpperCase() + err.detail.slice(1);
153
+ if (err.code) {
154
+ code = err.code;
155
+ expected = err.expected ?? err.detail;
156
+ }
157
+ else {
158
+ ;
159
+ ({ code, expected } = describeConstraint(err.detail));
160
+ }
161
+ }
162
+ }
163
+ if (path === '')
164
+ path = 'root';
165
+ const received = (0, error_1.formatReceivedValue)((0, error_1.getNestedValue)(data, path));
166
+ return { path, message, expected, received, code };
167
+ }
168
+ const MAX_ERRORS = 100;
169
+ /** Runs object-level `refine()` rules on a validated object */
170
+ function objectRulesCheck(rules) {
171
+ return (v) => {
172
+ for (const r of rules) {
173
+ if (!r.fn(v)) {
174
+ const message = typeof r.message === 'function'
175
+ ? r.message(v)
176
+ : r.message || 'Invalid value';
177
+ const err = new rules_1.RuleError('custom', message, 'CUSTOM_VALIDATION', 'custom refinement rule');
178
+ if (r.path)
179
+ err.path.push(...r.path.split('.'));
180
+ throw err;
181
+ }
182
+ }
183
+ return v;
184
+ };
185
+ }
30
186
  class TexEngine {
31
187
  constructor(schema, rawSchema, strict = false, isOptional = false) {
32
188
  this._isOptional = false;
33
189
  this._isNullable = false;
190
+ // Object-level refine() rules, run once every field is valid
191
+ this._objectRules = [];
34
192
  this.schema = schema;
35
193
  this.rawSchema = rawSchema;
36
194
  this.strict = strict;
37
195
  this._isOptional = isOptional;
38
- this.validator = (0, compile_1.compileSchema)(schema, strict);
196
+ this.validator = (0, compile_1.compileSchema)(schema, strict, rawSchema);
39
197
  }
40
198
  /**
41
199
  * The compiled validator stops at the first problem, which keeps valid
42
- * requests fast. Once a parse has failed, every other top-level field is
43
- * checked on its own so the response lists all failing fields (capped).
200
+ * requests fast. Once a parse has failed, every field is checked on its
201
+ * own, descending into nested objects, array items and record values, so
202
+ * the response lists every failing value (capped).
44
203
  */
45
- collectErrors(first, data) {
46
- const errors = [this.describeError(first, data)];
204
+ collectErrors(first, data, query) {
205
+ const firstErrors = first instanceof error_1.ValidatorError
206
+ ? [...first.errors]
207
+ : [describeRuleError(first, data)];
47
208
  if (data === null || typeof data !== 'object' || Array.isArray(data)) {
48
- return errors;
209
+ return firstErrors;
49
210
  }
50
- const MAX_ERRORS = 100;
51
- const failed = new Set(errors.map((e) => e.path.split(/[.[]/)[0]));
211
+ const errors = this._collectFields(data, query);
212
+ // Empty when only an object-level refine() failed
213
+ return errors.length > 0 ? errors : firstErrors;
214
+ }
215
+ /** @internal Checks each field on its own; used after a failed parse */
216
+ _collectFields(data, query = false) {
217
+ const errors = [];
52
218
  if (this.strict) {
53
219
  for (const key of Object.keys(data)) {
54
- if (errors.length >= MAX_ERRORS)
55
- return errors;
56
- if (!(key in this.schema) && !failed.has(key)) {
57
- failed.add(key);
220
+ if (!(key in this.schema)) {
58
221
  errors.push({
59
222
  path: key,
60
223
  message: 'Unknown field not allowed in strict mode',
@@ -69,120 +232,22 @@ class TexEngine {
69
232
  for (const key of Object.keys(this.schema)) {
70
233
  if (errors.length >= MAX_ERRORS)
71
234
  break;
72
- if (failed.has(key))
73
- continue;
74
- let validate = this._fieldValidators.get(key);
235
+ const cacheKey = query ? `?${key}` : key;
236
+ let validate = this._fieldValidators.get(cacheKey);
75
237
  if (!validate) {
76
- validate = (0, compile_1.compileSchema)({ [key]: this.schema[key] }, false);
77
- this._fieldValidators.set(key, validate);
78
- }
79
- try {
80
- validate(data);
81
- }
82
- catch (err) {
83
- errors.push(this.describeError(err, data));
84
- }
85
- }
86
- return errors;
87
- }
88
- /** Turns a validator exception into a structured error entry */
89
- describeError(err, data) {
90
- let path = 'root';
91
- let message = err.message || 'Validation failed';
92
- let expected;
93
- let code = 'VALIDATION_ERROR';
94
- if (message.startsWith('Missing required field: ')) {
95
- path = message.replace('Missing required field: ', '').trim();
96
- message = 'Expected value, received undefined';
97
- expected = 'defined';
98
- code = 'MISSING_REQUIRED_FIELD';
99
- }
100
- else if (message.startsWith("Field '")) {
101
- const match = message.match(/Field '([^']+)' (.+)/);
102
- if (match) {
103
- path = match[1];
104
- const constraint = match[2];
105
- message = constraint.charAt(0).toUpperCase() + constraint.slice(1);
106
- if (constraint.startsWith('must be a string')) {
107
- expected = 'string';
108
- code = 'INVALID_TYPE';
109
- }
110
- else if (constraint.startsWith('must be a number')) {
111
- expected = 'number';
112
- code = 'INVALID_TYPE';
113
- }
114
- else if (constraint.startsWith('must be a boolean')) {
115
- expected = 'boolean';
116
- code = 'INVALID_TYPE';
117
- }
118
- else if (constraint.startsWith('must be an array') ||
119
- constraint.includes('array must have') ||
120
- constraint.includes('array exceeds')) {
121
- expected = 'array';
122
- code = constraint.includes('array must have')
123
- ? 'ARRAY_TOO_SHORT'
124
- : constraint.includes('array exceeds')
125
- ? 'ARRAY_TOO_LONG'
126
- : 'INVALID_TYPE';
127
- }
128
- else if (constraint.startsWith('must be an object')) {
129
- expected = 'object';
130
- code = 'INVALID_TYPE';
131
- }
132
- else if (constraint.startsWith('must be a valid email')) {
133
- expected = 'valid email address';
134
- code = 'INVALID_EMAIL';
135
- }
136
- else if (constraint.startsWith('must be one of')) {
137
- expected = constraint.replace('must be one of', '').trim();
138
- code = 'INVALID_ENUM_VALUE';
139
- }
140
- else if (constraint.startsWith('must be at least') ||
141
- constraint.startsWith('must be >=')) {
142
- expected = constraint;
143
- code = 'VALUE_TOO_SMALL';
144
- }
145
- else if (constraint.startsWith('must be at most') ||
146
- constraint.startsWith('must be <=')) {
147
- expected = constraint;
148
- code = 'VALUE_TOO_LARGE';
149
- }
150
- else if (constraint.startsWith('must contain a number')) {
151
- expected = 'must contain at least one digit';
152
- code = 'PASSWORD_REQUIRES_NUMBER';
153
- }
154
- else if (constraint.startsWith('must contain an uppercase')) {
155
- expected = 'must contain at least one uppercase letter';
156
- code = 'PASSWORD_REQUIRES_UPPERCASE';
157
- }
158
- else if (constraint.startsWith('must contain a lowercase')) {
159
- expected = 'must contain at least one lowercase letter';
160
- code = 'PASSWORD_REQUIRES_LOWERCASE';
161
- }
162
- else if (constraint.startsWith('must contain a symbol')) {
163
- expected = 'must contain at least one symbol';
164
- code = 'PASSWORD_REQUIRES_SYMBOL';
165
- }
166
- else {
167
- expected = constraint;
168
- }
169
- }
170
- }
171
- else if (message.startsWith("Strict mode error: Unknown field '")) {
172
- const match = message.match(/Strict mode error: Unknown field '([^']+)'(?: at path '([^']*)')?/);
173
- if (match) {
174
- const key = match[1];
175
- const parentPath = match[2];
176
- path =
177
- parentPath && parentPath.length > 0 ? `${parentPath}.${key}` : key;
178
- message = 'Unknown field not allowed in strict mode';
179
- expected = 'undefined (field omitted)';
180
- code = 'UNRECOGNIZED_KEYS';
238
+ validate = (0, compile_1.compileSchema)({ [key]: this.schema[key] }, false, { [key]: this.rawSchema[key] }, query);
239
+ this._fieldValidators.set(cacheKey, validate);
181
240
  }
241
+ const found = errorsOf(validate, data);
242
+ if (found.length === 0)
243
+ continue;
244
+ // The failure is inside a nested object or array: list every part
245
+ const inner = found.some((e) => e.path !== key)
246
+ ? collectInner(this.rawSchema[key], data[key], key)
247
+ : [];
248
+ errors.push(...(inner.length > 0 ? inner : found));
182
249
  }
183
- const receivedRaw = (0, error_1.getNestedValue)(data, path);
184
- const received = (0, error_1.formatReceivedValue)(receivedRaw);
185
- return { path, message, expected, received, code };
250
+ return errors.length > MAX_ERRORS ? errors.slice(0, MAX_ERRORS) : errors;
186
251
  }
187
252
  getCompiledSchema() {
188
253
  return this.schema;
@@ -193,18 +258,45 @@ class TexEngine {
193
258
  engine._isNullable = this._isNullable;
194
259
  return engine;
195
260
  }
261
+ /** A copy with the same shape, keeping refine() rules */
262
+ copy() {
263
+ const engine = this.derive(this.schema, this.rawSchema);
264
+ engine._objectRules = this._objectRules;
265
+ engine._objectCheck = this._objectCheck;
266
+ return engine;
267
+ }
196
268
  /** The field may be left out. */
197
269
  optional() {
198
- const engine = this.derive(this.schema, this.rawSchema);
270
+ const engine = this.copy();
199
271
  engine._isOptional = true;
200
272
  return engine;
201
273
  }
202
274
  /** The field may be `null`. */
203
275
  nullable() {
204
- const engine = this.derive(this.schema, this.rawSchema);
276
+ const engine = this.copy();
205
277
  engine._isNullable = true;
206
278
  return engine;
207
279
  }
280
+ /**
281
+ * A check across fields, run once every field is valid. `path` names the
282
+ * field the error is reported on (default: the object itself).
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * tex.object({ password: tex.string(), confirm: tex.string() })
287
+ * .refine((d) => d.password === d.confirm, { message: 'Passwords must match', path: 'confirm' })
288
+ * ```
289
+ */
290
+ refine(fn, message) {
291
+ const opts = message !== null && typeof message === 'object' ? message : { message };
292
+ const engine = this.copy();
293
+ engine._objectRules = [
294
+ ...this._objectRules,
295
+ { fn, message: opts.message, path: opts.path },
296
+ ];
297
+ engine._objectCheck = objectRulesCheck(engine._objectRules);
298
+ return engine;
299
+ }
208
300
  /** The field may be left out or be `null`. */
209
301
  nullish() {
210
302
  return this.optional().nullable();
@@ -216,14 +308,12 @@ class TexEngine {
216
308
  // Not `val.includes('?')`: a nested rule contains its children's `?`
217
309
  newSchema[key] = `${val} | optional`;
218
310
  const rawVal = this.rawSchema[key];
219
- if (rawVal instanceof tex_types_1.TexType) {
220
- newRaw[key] = new tex_types_1.TexType(newSchema[key]);
221
- newRaw[key].sanitizers = rawVal.sanitizers;
222
- newRaw[key].refinements = rawVal.refinements;
223
- }
224
- else if (rawVal instanceof TexEngine) {
311
+ if (rawVal instanceof tex_types_1.TexType || rawVal instanceof TexEngine) {
225
312
  newRaw[key] = rawVal.optional();
226
313
  }
314
+ else if (rawVal !== null && typeof rawVal === 'object') {
315
+ newRaw[key] = rawVal;
316
+ }
227
317
  else {
228
318
  newRaw[key] = newSchema[key];
229
319
  }
@@ -269,22 +359,24 @@ class TexEngine {
269
359
  }
270
360
  return this.derive(newSchema, newRaw);
271
361
  }
362
+ /**
363
+ * Validates `data` and returns a new object with the declared fields only.
364
+ * The input is never modified. Throws `ValidatorError` listing every
365
+ * failing field.
366
+ */
272
367
  parse(data) {
273
- const plan = this._plan || (this._plan = splitPlan(buildPlan(this.rawSchema)));
274
- const coerceScalars = Boolean(this._isQueryOrParam);
275
- // 1. Defaults, empty-string handling, coercion and sanitizers. Only
276
- // fields that need any of it are visited.
277
- if (data && typeof data === 'object') {
278
- const prep = coerceScalars ? plan.prepQuery : plan.prep;
279
- for (const f of prep)
280
- prepareField(data, f, coerceScalars);
281
- }
282
- // 2. Type validation (compiled once per schema)
283
- let parsedData;
368
+ const query = Boolean(this._isQueryOrParam);
369
+ const validate = query
370
+ ? (this._queryValidator ?? (this._queryValidator = (0, compile_1.compileSchema)(this.schema, this.strict, this.rawSchema, true)))
371
+ : this.validator;
284
372
  try {
285
- parsedData = this.validator(data);
373
+ const result = validate(data);
374
+ return this._objectCheck ? this._objectCheck(result) : result;
286
375
  }
287
376
  catch (err) {
377
+ if (!(err instanceof rules_1.RuleError) && !(err instanceof error_1.ValidatorError)) {
378
+ throw err;
379
+ }
288
380
  if (data === process.env &&
289
381
  (process.env.__EXIS_SKIP_ENV_CHECK === 'true' ||
290
382
  process.env.EXIS_SKIP_ENV_CHECK === 'true')) {
@@ -304,77 +396,24 @@ class TexEngine {
304
396
  }
305
397
  return mock;
306
398
  }
307
- throw new error_1.ValidatorError(this.collectErrors(err, data));
308
- }
309
- if (parsedData && typeof parsedData === 'object' && plan.post.length > 0) {
310
- const errors = [];
311
- for (const f of plan.post) {
312
- const key = f.key;
313
- // Preserve Date objects and ensure defaults are applied
314
- if (parsedData[key] === undefined &&
315
- f.tex?.defaultValue !== undefined) {
316
- parsedData[key] = resolveDefault(f.tex.defaultValue);
317
- }
318
- if (f.isDate && data && data[key] instanceof Date) {
319
- parsedData[key] = data[key];
320
- }
321
- // 3. Post-validation synchronous refinements
322
- if (f.hasRefinements && parsedData[key] !== undefined) {
323
- if (!(parsedData[key] === null && f.isNullable)) {
324
- runSyncRefinements(f, parsedData[key], errors);
325
- }
326
- }
327
- }
328
- if (errors.length > 0)
329
- throw new error_1.ValidatorError(errors);
330
- // 4. Post-validation field transformations
331
- for (const f of plan.post) {
332
- if (f.tex && f.tex.transformations.length > 0) {
333
- const key = f.key;
334
- if (parsedData[key] !== undefined) {
335
- for (const t of f.tex.transformations) {
336
- parsedData[key] = t(parsedData[key]);
337
- }
338
- }
339
- }
340
- }
399
+ throw new error_1.ValidatorError(this.collectErrors(err, data, query));
341
400
  }
342
- return parsedData;
343
401
  }
402
+ /** Like `parse()`, but returns `{ success, data }` or `{ success, errors }`. */
403
+ safeParse(data) {
404
+ return safely(() => this.parse(data));
405
+ }
406
+ /** Runs `parse()`, then every `refineAsync()` rule, nested ones included. */
344
407
  async parseAsync(data) {
345
- const parsed = this.parse(data); // Handles sync sanitizers + rust + sync refine + transformations
346
- const errors = [];
347
- if (parsed && typeof parsed === 'object') {
348
- await Promise.all(Object.entries(this.rawSchema).map(async ([key, val]) => {
349
- const valData = parsed[key];
350
- const isNullableField = val instanceof tex_types_1.TexType &&
351
- (val._raw.includes('nullable') || val._raw.includes('nullish'));
352
- if (valData === null && isNullableField)
353
- return;
354
- if (valData !== undefined &&
355
- val instanceof tex_types_1.TexType &&
356
- val.refinements.length > 0) {
357
- for (const r of val.refinements) {
358
- if (r.async && !(await r.fn(valData))) {
359
- const msg = typeof r.message === 'function'
360
- ? r.message(valData)
361
- : r.message || 'Invalid value';
362
- errors.push({
363
- path: key,
364
- message: msg,
365
- expected: 'custom async refinement rule',
366
- received: (0, error_1.formatReceivedValue)(valData),
367
- code: 'CUSTOM_VALIDATION',
368
- });
369
- }
370
- }
371
- }
372
- }));
373
- }
408
+ const parsed = this.parse(data);
409
+ const errors = await runAsyncRefinements(this, parsed);
374
410
  if (errors.length > 0)
375
411
  throw new error_1.ValidatorError(errors);
376
412
  return parsed;
377
413
  }
414
+ safeParseAsync(data) {
415
+ return safelyAsync(() => this.parseAsync(data));
416
+ }
378
417
  toMongoSchema() {
379
418
  const schemaDef = {};
380
419
  for (const [key, rawDef] of Object.entries(this.schema)) {
@@ -384,6 +423,7 @@ class TexEngine {
384
423
  const field = {};
385
424
  if (baseType.startsWith('string') ||
386
425
  baseType.startsWith('email') ||
426
+ baseType.startsWith('url') ||
387
427
  baseType.startsWith('uuid') ||
388
428
  baseType.startsWith('cuid') ||
389
429
  baseType.startsWith('password')) {
@@ -414,15 +454,16 @@ class TexEngine {
414
454
  const rawDef = this.schema[key] || '';
415
455
  const typeStr = rawDef.split('|')[0].trim();
416
456
  // A nested rule's text includes its children's modifiers
417
- const isOptional = rawVal instanceof TexEngine
418
- ? rawVal._isOptional
457
+ const engine = rawVal instanceof TexTransformedEngine ? rawVal.inner : rawVal;
458
+ const isOptional = engine instanceof TexEngine
459
+ ? engine._isOptional || / \| optional$/.test(rawDef)
419
460
  : typeStr.endsWith('?') || rawDef.includes('optional');
420
461
  let prop;
421
462
  if (rawVal && typeof rawVal.toOpenApi === 'function') {
422
463
  prop = rawVal.toOpenApi();
423
464
  }
424
465
  else {
425
- const baseType = typeStr.replace('?', '');
466
+ const baseType = (0, rules_1.ruleBase)(rawDef);
426
467
  prop = {};
427
468
  if (baseType.startsWith('string') ||
428
469
  baseType.startsWith('uuid') ||
@@ -434,6 +475,10 @@ class TexEngine {
434
475
  prop.type = 'string';
435
476
  prop.format = 'email';
436
477
  }
478
+ else if (baseType.startsWith('url')) {
479
+ prop.type = 'string';
480
+ prop.format = 'uri';
481
+ }
437
482
  else if (baseType.startsWith('number')) {
438
483
  prop.type = 'number';
439
484
  }
@@ -449,7 +494,7 @@ class TexEngine {
449
494
  }
450
495
  else if (baseType.startsWith('enum:')) {
451
496
  prop.type = 'string';
452
- prop.enum = baseType.replace('enum:', '').split(',');
497
+ prop.enum = (0, rules_1.parseEnumValues)(baseType.slice(5));
453
498
  }
454
499
  else {
455
500
  prop.type = 'object';
@@ -462,6 +507,8 @@ class TexEngine {
462
507
  const result = { type: 'object', properties };
463
508
  if (required.length > 0)
464
509
  result.required = required;
510
+ if (this.strict)
511
+ result.additionalProperties = false;
465
512
  return result;
466
513
  }
467
514
  }
@@ -475,10 +522,16 @@ class TexTransformedEngine {
475
522
  const validated = this.inner.parse(data);
476
523
  return this.transformFn(validated);
477
524
  }
525
+ safeParse(data) {
526
+ return safely(() => this.parse(data));
527
+ }
478
528
  async parseAsync(data) {
479
529
  const validated = await this.inner.parseAsync(data);
480
530
  return this.transformFn(validated);
481
531
  }
532
+ safeParseAsync(data) {
533
+ return safelyAsync(() => this.parseAsync(data));
534
+ }
482
535
  toOpenApi() {
483
536
  return this.inner.toOpenApi();
484
537
  }
@@ -490,14 +543,30 @@ class TexDiscriminatedUnionEngine {
490
543
  this.variants = variants;
491
544
  this.engines = variants.map((v) => v instanceof TexEngine
492
545
  ? v
493
- : new TexEngine(Object.fromEntries(Object.entries(v).map(([k, val]) => [
494
- k,
495
- val instanceof tex_types_1.TexType ? val._raw : String(val),
496
- ])), v));
546
+ : new TexEngine(Object.fromEntries(Object.entries(v).map(([k, val]) => [k, fieldRule(k, val)])), v));
547
+ this.tags = this.engines.map((engine) => {
548
+ const rule = engine.getCompiledSchema()[discriminator];
549
+ if (rule === undefined)
550
+ return [];
551
+ const base = (0, rules_1.ruleBase)(rule);
552
+ if (base.startsWith('literal:'))
553
+ return [(0, rules_1.parseLiteral)(base.slice(8))];
554
+ if (base.startsWith('enum:'))
555
+ return (0, rules_1.parseEnumValues)(base.slice(5));
556
+ return [];
557
+ });
497
558
  }
498
- parse(data) {
559
+ /** The variant whose discriminator matches `data`, if any */
560
+ variantFor(data) {
561
+ if (!data || typeof data !== 'object')
562
+ return undefined;
563
+ const tag = data[this.discriminator];
564
+ const i = this.tags.findIndex((tags) => tags.includes(tag));
565
+ return i === -1 ? undefined : this.engines[i];
566
+ }
567
+ noMatch(data) {
499
568
  if (!data || typeof data !== 'object') {
500
- throw new error_1.ValidatorError([
569
+ return new error_1.ValidatorError([
501
570
  {
502
571
  path: '',
503
572
  message: 'Expected an object for discriminated union payload',
@@ -509,7 +578,7 @@ class TexDiscriminatedUnionEngine {
509
578
  }
510
579
  const tagValue = data[this.discriminator];
511
580
  if (tagValue === undefined) {
512
- throw new error_1.ValidatorError([
581
+ return new error_1.ValidatorError([
513
582
  {
514
583
  path: this.discriminator,
515
584
  message: `Missing discriminator field '${this.discriminator}'`,
@@ -519,83 +588,33 @@ class TexDiscriminatedUnionEngine {
519
588
  },
520
589
  ]);
521
590
  }
522
- for (const engine of this.engines) {
523
- const rawField = engine.rawSchema?.[this.discriminator];
524
- let matches = false;
525
- if (rawField instanceof tex_types_1.TexType) {
526
- if (rawField._raw.startsWith('literal:')) {
527
- const literalVal = rawField._raw
528
- .replace('literal:', '')
529
- .split('|')[0]
530
- .trim();
531
- if (String(tagValue) === literalVal)
532
- matches = true;
533
- }
534
- else if (rawField._raw.startsWith('enum:')) {
535
- const allowed = rawField._raw
536
- .replace('enum:', '')
537
- .split('|')[0]
538
- .trim()
539
- .split(',');
540
- if (allowed.includes(String(tagValue)))
541
- matches = true;
542
- }
543
- }
544
- else if (typeof rawField === 'string' &&
545
- rawField.startsWith('literal:')) {
546
- const literalVal = rawField.replace('literal:', '').split('|')[0].trim();
547
- if (String(tagValue) === literalVal)
548
- matches = true;
549
- }
550
- if (matches) {
551
- return engine.parse(data);
552
- }
553
- }
554
- throw new error_1.ValidatorError([
591
+ return new error_1.ValidatorError([
555
592
  {
556
593
  path: this.discriminator,
557
594
  message: `Invalid discriminator value '${tagValue}' for field '${this.discriminator}'`,
558
- expected: 'one of matching variant discriminators',
595
+ expected: `one of ${JSON.stringify(this.tags.flat())}`,
559
596
  received: (0, error_1.formatReceivedValue)(tagValue),
560
597
  code: 'INVALID_DISCRIMINATOR_VALUE',
561
598
  },
562
599
  ]);
563
600
  }
601
+ parse(data) {
602
+ const engine = this.variantFor(data);
603
+ if (!engine)
604
+ throw this.noMatch(data);
605
+ return engine.parse(data);
606
+ }
607
+ safeParse(data) {
608
+ return safely(() => this.parse(data));
609
+ }
564
610
  async parseAsync(data) {
565
- if (!data || typeof data !== 'object') {
566
- return this.parse(data);
567
- }
568
- const tagValue = data[this.discriminator];
569
- if (tagValue === undefined) {
570
- return this.parse(data);
571
- }
572
- for (const engine of this.engines) {
573
- const rawField = engine.rawSchema?.[this.discriminator];
574
- let matches = false;
575
- if (rawField instanceof tex_types_1.TexType) {
576
- if (rawField._raw.startsWith('literal:')) {
577
- const literalVal = rawField._raw
578
- .replace('literal:', '')
579
- .split('|')[0]
580
- .trim();
581
- if (String(tagValue) === literalVal)
582
- matches = true;
583
- }
584
- else if (rawField._raw.startsWith('enum:')) {
585
- const allowed = rawField._raw
586
- .replace('enum:', '')
587
- .split('|')[0]
588
- .trim()
589
- .split(',');
590
- if (allowed.includes(String(tagValue)))
591
- matches = true;
592
- }
593
- }
594
- if (matches) {
595
- return engine.parseAsync(data);
596
- }
597
- }
598
- return this.parse(data);
611
+ const engine = this.variantFor(data);
612
+ if (!engine)
613
+ throw this.noMatch(data);
614
+ return engine.parseAsync(data);
615
+ }
616
+ safeParseAsync(data) {
617
+ return safelyAsync(() => this.parseAsync(data));
599
618
  }
600
619
  toOpenApi() {
601
620
  const oneOf = this.engines.map((e) => e.toOpenApi());
@@ -608,259 +627,147 @@ class TexDiscriminatedUnionEngine {
608
627
  }
609
628
  }
610
629
  exports.TexDiscriminatedUnionEngine = TexDiscriminatedUnionEngine;
611
- function needsPrep(f, coerceScalars) {
612
- return (f.tex?.defaultValue !== undefined ||
613
- f.emptyString !== 'keep' ||
614
- f.isDate ||
615
- f.stringCoerce ||
616
- f.number === 'coerce' ||
617
- f.boolean === 'coerce' ||
618
- (coerceScalars && (f.number === 'query' || f.boolean === 'query')) ||
619
- f.array !== 'no' ||
620
- (f.tex !== null && f.tex.sanitizers.length > 0) ||
621
- (f.item !== null && f.item.sanitizers.length > 0));
622
- }
623
- function splitPlan(plan) {
624
- return {
625
- prep: plan.filter((f) => needsPrep(f, false)),
626
- prepQuery: plan.filter((f) => needsPrep(f, true)),
627
- post: plan.filter((f) => f.tex?.defaultValue !== undefined ||
628
- f.isDate ||
629
- f.hasRefinements ||
630
- (f.tex !== null && f.tex.transformations.length > 0)),
631
- };
632
- }
633
- function scalarMode(plain, raw, type, coerce) {
634
- if (!plain || !raw.startsWith(type))
635
- return 'no';
636
- return coerce ? 'coerce' : 'query';
637
- }
638
- function arrayMode(plain, raw, coerce) {
639
- if (!plain || !coerce || !raw.startsWith('array<'))
640
- return 'no';
641
- if (raw.startsWith('array<number'))
642
- return 'number';
643
- if (raw.startsWith('array<boolean'))
644
- return 'boolean';
645
- return 'other';
630
+ // ─── Error collection ────────────────────────────────────────────────────────
631
+ function errorsOf(validate, data) {
632
+ try {
633
+ validate(data);
634
+ return [];
635
+ }
636
+ catch (err) {
637
+ if (err instanceof error_1.ValidatorError)
638
+ return [...err.errors];
639
+ if (err instanceof rules_1.RuleError)
640
+ return [describeRuleError(err, data)];
641
+ throw err;
642
+ }
646
643
  }
647
- function buildPlan(rawSchema) {
648
- const plan = [];
649
- for (const key of Object.keys(rawSchema)) {
650
- const val = rawSchema[key];
651
- const tex = val instanceof tex_types_1.TexType ? val : null;
652
- // A nested schema contributes only its own modifiers, not its children's
653
- const raw = tex
654
- ? tex._raw
655
- : val instanceof TexEngine
656
- ? engineModifiers(val)
657
- : String(val);
658
- // Unions coerce per alternative inside the compiled rule
659
- const plain = tex !== null && !raw.includes('||');
660
- const coerce = raw.includes('coerce');
661
- const nullable = raw.includes('nullable');
662
- const nullish = raw.includes('nullish');
663
- const optional = raw.includes('optional');
664
- let emptyString = 'keep';
665
- if (optional || nullable || nullish || raw.includes('?')) {
666
- emptyString = nullable && !optional && !nullish ? 'null' : 'delete';
667
- }
668
- const item = val?._item instanceof tex_types_1.TexType ? val._item : null;
669
- const isDate = plain && raw.startsWith('date');
670
- plan.push({
671
- key,
672
- tex,
673
- emptyString,
674
- isDate,
675
- dateCoerce: isDate && coerce,
676
- minDate: isDate ? raw.match(/minDate:([^\s|]+)/)?.[1] : undefined,
677
- maxDate: isDate ? raw.match(/maxDate:([^\s|]+)/)?.[1] : undefined,
678
- stringCoerce: plain && coerce && raw.startsWith('string'),
679
- number: scalarMode(plain, raw, 'number', coerce),
680
- boolean: scalarMode(plain, raw, 'boolean', coerce),
681
- array: arrayMode(plain, raw, coerce),
682
- item,
683
- isNullable: tex !== null && (nullable || nullish),
684
- hasRefinements: (tex !== null && tex.refinements.length > 0) ||
685
- (item !== null && item.refinements.length > 0),
644
+ // Validates one item of an array (or one value of a record) at a time
645
+ const itemValidators = new WeakMap();
646
+ function itemValidator(container, isArray) {
647
+ let validate = itemValidators.get(container);
648
+ if (!validate) {
649
+ const item = isArray ? container._item : container._value;
650
+ const coerce = isArray && (0, rules_1.ruleParts)(container._raw).includes('coerce');
651
+ const rule = isArray
652
+ ? `array<${fieldRule('[item]', item)}>${coerce ? ' | coerce' : ''}`
653
+ : `record<${fieldRule('[value]', item)}>`;
654
+ validate = (0, compile_1.compileSchema)({ v: rule }, false, {
655
+ v: isArray ? { _item: item } : { _value: item },
686
656
  });
657
+ itemValidators.set(container, validate);
687
658
  }
688
- return plan;
689
- }
690
- function resolveDefault(defaultValue) {
691
- return typeof defaultValue === 'function' ? defaultValue() : defaultValue;
659
+ return validate;
692
660
  }
693
- function fieldError(key, value, message, expected, code) {
694
- throw new error_1.ValidatorError([
695
- {
696
- path: key,
697
- message,
698
- expected,
699
- received: (0, error_1.formatReceivedValue)(value),
700
- code,
701
- },
702
- ]);
661
+ function relabel(errors, from, to) {
662
+ return errors.map((e) => ({
663
+ ...e,
664
+ path: e.path.startsWith(from) ? to + e.path.slice(from.length) : to,
665
+ }));
703
666
  }
704
- function prepareDate(key, value, f) {
705
- if (f.dateCoerce &&
706
- (typeof value === 'string' || typeof value === 'number')) {
707
- const d = new Date(value);
708
- if (isNaN(d.getTime())) {
709
- fieldError(key, value, 'Must be a valid date', 'valid date string or timestamp', 'INVALID_DATE');
710
- }
711
- value = d;
712
- }
713
- if (!(value instanceof Date) &&
714
- typeof value !== 'string' &&
715
- typeof value !== 'number') {
716
- fieldError(key, value, 'Must be a valid date', 'valid date or date string', 'INVALID_TYPE');
717
- }
718
- if (value instanceof Date && isNaN(value.getTime())) {
719
- fieldError(key, value, 'Must be a valid date', 'valid date string or timestamp', 'INVALID_DATE');
720
- }
721
- const time = new Date(value).getTime();
722
- if (f.minDate && time < new Date(f.minDate).getTime()) {
723
- fieldError(key, value, `Date must be after ${f.minDate}`, `>= ${f.minDate}`, 'DATE_TOO_EARLY');
724
- }
725
- if (f.maxDate && time > new Date(f.maxDate).getTime()) {
726
- fieldError(key, value, `Date must be before ${f.maxDate}`, `<= ${f.maxDate}`, 'DATE_TOO_LATE');
667
+ /** Every failing value inside a nested object, array or record at `path` */
668
+ function collectInner(raw, value, path) {
669
+ if (value === null || typeof value !== 'object')
670
+ return [];
671
+ const engine = raw instanceof TexEngine
672
+ ? raw
673
+ : raw instanceof TexTransformedEngine
674
+ ? raw.inner
675
+ : undefined;
676
+ if (engine) {
677
+ if (Array.isArray(value))
678
+ return [];
679
+ return engine._collectFields(value).map((e) => ({
680
+ ...e,
681
+ path: e.path === 'root' ? path : `${path}.${e.path}`,
682
+ }));
683
+ }
684
+ if (!(raw instanceof tex_types_1.TexType))
685
+ return [];
686
+ const isArray = Array.isArray(value);
687
+ const item = isArray ? raw._item : raw._value;
688
+ if (item === undefined)
689
+ return [];
690
+ const validate = itemValidator(raw, isArray);
691
+ const keys = isArray
692
+ ? value.map((_, i) => i)
693
+ : Object.keys(value).filter((k) => k !== '__proto__' && k !== 'constructor');
694
+ const out = [];
695
+ for (const k of keys) {
696
+ if (out.length >= MAX_ERRORS)
697
+ break;
698
+ const single = isArray ? [value[k]] : { [k]: value[k] };
699
+ const at = isArray ? `v[0]` : `v.${k}`;
700
+ const itemPath = isArray ? `${path}[${k}]` : `${path}.${k}`;
701
+ const found = errorsOf(validate, { v: single });
702
+ if (found.length === 0)
703
+ continue;
704
+ const nested = found.some((e) => e.path !== at)
705
+ ? collectInner(item, value[k], itemPath)
706
+ : [];
707
+ out.push(...(nested.length > 0 ? nested : relabel(found, at, itemPath)));
727
708
  }
728
- return value;
709
+ return out;
729
710
  }
730
- function prepareField(data, f, coerceScalars) {
731
- const key = f.key;
732
- const tex = f.tex;
733
- // Apply default value / lazy factory function if field is omitted
734
- if (data[key] === undefined && tex?.defaultValue !== undefined) {
735
- data[key] = resolveDefault(tex.defaultValue);
736
- }
737
- if (f.emptyString !== 'keep' &&
738
- typeof data[key] === 'string' &&
739
- data[key].trim() === '') {
740
- if (f.emptyString === 'null') {
741
- data[key] = null;
742
- }
743
- else {
744
- delete data[key];
745
- if (tex?.defaultValue !== undefined) {
746
- data[key] = resolveDefault(tex.defaultValue);
747
- }
748
- }
749
- }
750
- let value = data[key];
711
+ // ─── Async refinements ───────────────────────────────────────────────────────
712
+ // Walks the parsed value next to its schema, so `refineAsync()` rules run at
713
+ // every depth: nested objects, array items and record values.
714
+ function joinPath(path, key) {
715
+ if (typeof key === 'number')
716
+ return `${path}[${key}]`;
717
+ return path ? `${path}.${key}` : key;
718
+ }
719
+ function collectAsync(node, value, path, tasks) {
751
720
  if (value === undefined || value === null)
752
721
  return;
753
- if (f.isDate)
754
- data[key] = value = prepareDate(key, value, f);
755
- if (f.stringCoerce && typeof value !== 'string') {
756
- data[key] = value = String(value);
757
- }
758
- if ((f.number === 'coerce' || (f.number === 'query' && coerceScalars)) &&
759
- typeof value === 'string' &&
760
- value.trim() !== '') {
761
- const n = Number(value);
762
- if (isNaN(n))
763
- fieldError(key, value, 'Must be a number', 'number', 'INVALID_TYPE');
764
- data[key] = value = n;
765
- }
766
- if ((f.boolean === 'coerce' || (f.boolean === 'query' && coerceScalars)) &&
767
- typeof value === 'string' &&
768
- value.trim() !== '') {
769
- const lower = value.toLowerCase().trim();
770
- if (lower === 'true' || lower === '1')
771
- data[key] = value = true;
772
- else if (lower === 'false' || lower === '0')
773
- data[key] = value = false;
774
- else {
775
- fieldError(key, value, 'Must be a boolean', 'boolean (true or false)', 'INVALID_TYPE');
722
+ if (node instanceof TexEngine) {
723
+ if (typeof value !== 'object')
724
+ return;
725
+ const raw = node.rawSchema;
726
+ for (const key of Object.keys(raw)) {
727
+ collectAsync(raw[key], value[key], joinPath(path, key), tasks);
776
728
  }
729
+ return;
777
730
  }
778
- if (f.array !== 'no')
779
- data[key] = coerceArray(value, f.array);
780
- if (tex !== null && tex.sanitizers.length > 0) {
781
- for (const s of tex.sanitizers) {
782
- if (data[key] === undefined || data[key] === null)
783
- break;
784
- data[key] = s(data[key]);
785
- }
731
+ if (node instanceof TexDiscriminatedUnionEngine) {
732
+ const engine = node.variantFor(value);
733
+ if (engine)
734
+ collectAsync(engine, value, path, tasks);
735
+ return;
786
736
  }
787
- const item = f.item;
788
- if (item !== null && item.sanitizers.length > 0 && Array.isArray(data[key])) {
789
- const arr = data[key];
790
- for (let i = 0; i < arr.length; i++) {
791
- for (const s of item.sanitizers) {
792
- if (arr[i] === undefined || arr[i] === null)
793
- break;
794
- arr[i] = s(arr[i]);
795
- }
796
- }
737
+ if (!(node instanceof tex_types_1.TexType))
738
+ return;
739
+ for (const r of node.refinements) {
740
+ if (!r.async)
741
+ continue;
742
+ tasks.push(Promise.resolve(r.fn(value)).then((ok) => ok
743
+ ? null
744
+ : {
745
+ path: path || 'root',
746
+ message: (0, rules_1.refinementMessage)(r, value),
747
+ expected: 'custom async refinement rule',
748
+ received: (0, error_1.formatReceivedValue)(value),
749
+ code: 'CUSTOM_VALIDATION',
750
+ }));
797
751
  }
798
- }
799
- function coerceItem(item, kind) {
800
- if (typeof item !== 'string')
801
- return item;
802
- if (kind === 'number') {
803
- const n = Number(item);
804
- return isNaN(n) ? item : n;
805
- }
806
- if (kind === 'boolean') {
807
- const lower = item.toLowerCase();
808
- if (lower === 'true')
809
- return true;
810
- if (lower === 'false')
811
- return false;
812
- }
813
- return item;
814
- }
815
- function coerceArray(value, kind) {
816
- if (!Array.isArray(value)) {
817
- if (typeof value !== 'string')
818
- return [value];
819
- const parts = value.includes(',')
820
- ? value.split(',').map((s) => s.trim())
821
- : [value];
822
- return kind === 'other' ? parts : parts.map((p) => coerceItem(p, kind));
823
- }
824
- return kind === 'other' ? value : value.map((v) => coerceItem(v, kind));
825
- }
826
- function refinementMessage(r, value) {
827
- return typeof r.message === 'function'
828
- ? r.message(value)
829
- : r.message || 'Invalid value';
830
- }
831
- function runSyncRefinements(f, value, errors) {
832
- if (f.tex !== null) {
833
- for (const r of f.tex.refinements) {
834
- if (!r.async && !r.fn(value)) {
835
- errors.push({
836
- path: f.key,
837
- message: refinementMessage(r, value),
838
- expected: 'custom refinement rule',
839
- received: (0, error_1.formatReceivedValue)(value),
840
- code: 'CUSTOM_VALIDATION',
841
- });
842
- }
752
+ const item = node._item;
753
+ if (item && Array.isArray(value)) {
754
+ for (let i = 0; i < value.length; i++) {
755
+ collectAsync(item, value[i], joinPath(path, i), tasks);
843
756
  }
844
757
  }
845
- const item = f.item;
846
- if (item === null || item.refinements.length === 0 || !Array.isArray(value)) {
847
- return;
848
- }
849
- const itemNullable = item._raw.includes('nullable') || item._raw.includes('nullish');
850
- for (let i = 0; i < value.length; i++) {
851
- if (value[i] === null && itemNullable)
852
- continue;
853
- for (const r of item.refinements) {
854
- if (!r.async && !r.fn(value[i])) {
855
- errors.push({
856
- path: `${f.key}[${i}]`,
857
- message: refinementMessage(r, value[i]),
858
- expected: 'custom refinement rule',
859
- received: (0, error_1.formatReceivedValue)(value[i]),
860
- code: 'CUSTOM_VALIDATION',
861
- });
862
- }
758
+ const recordValue = node._value;
759
+ if (recordValue && typeof value === 'object') {
760
+ for (const key of Object.keys(value)) {
761
+ collectAsync(recordValue, value[key], joinPath(path, key), tasks);
863
762
  }
864
763
  }
865
764
  }
765
+ async function runAsyncRefinements(engine, parsed) {
766
+ const tasks = [];
767
+ collectAsync(engine, parsed, '', tasks);
768
+ if (tasks.length === 0)
769
+ return [];
770
+ const results = await Promise.all(tasks);
771
+ return results.filter((e) => e !== null);
772
+ }
866
773
  //# sourceMappingURL=engine.js.map