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.
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +2 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +5 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/database/lifecycle.d.ts +19 -2
- package/dist/database/lifecycle.d.ts.map +1 -1
- package/dist/database/lifecycle.js +92 -53
- package/dist/database/lifecycle.js.map +1 -1
- package/dist/decorators/transaction.d.ts +0 -21
- package/dist/decorators/transaction.d.ts.map +1 -1
- package/dist/decorators/transaction.js +52 -32
- package/dist/decorators/transaction.js.map +1 -1
- package/dist/docs/content/boundaries.mdx +2 -0
- package/dist/docs/content/config.mdx +4 -2
- package/dist/docs/content/decorators.mdx +1 -1
- package/dist/docs/content/middleware.mdx +4 -2
- package/dist/docs/content/requests.mdx +2 -0
- package/dist/docs/content/responses.mdx +54 -0
- package/dist/docs/content/validation.mdx +46 -11
- package/dist/docs/reference/README.md +16 -16
- package/dist/docs/reference/config.mdx +2 -1
- package/dist/docs/reference/database.mdx +3 -2
- package/dist/docs/reference/middleware.mdx +1 -1
- package/dist/docs/reference/sanitize.mdx +129 -134
- package/dist/docs/reference/validator.mdx +11 -3
- package/dist/middleware/compression.d.ts.map +1 -1
- package/dist/middleware/compression.js +6 -5
- package/dist/middleware/compression.js.map +1 -1
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +47 -34
- package/dist/middleware/middleware.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +4 -7
- package/dist/router/controller-registrar.js.map +1 -1
- package/dist/router/route-scanner.d.ts +12 -4
- package/dist/router/route-scanner.d.ts.map +1 -1
- package/dist/router/route-scanner.js +46 -31
- package/dist/router/route-scanner.js.map +1 -1
- package/dist/router/router.d.ts +3 -0
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +40 -35
- package/dist/router/router.js.map +1 -1
- package/dist/router/scanner/boundary-loader.d.ts.map +1 -1
- package/dist/router/scanner/boundary-loader.js +25 -30
- package/dist/router/scanner/boundary-loader.js.map +1 -1
- package/dist/router/scanner/functional-compiler.d.ts.map +1 -1
- package/dist/router/scanner/functional-compiler.js +29 -29
- package/dist/router/scanner/functional-compiler.js.map +1 -1
- package/dist/sanitize/index.d.ts +29 -3
- package/dist/sanitize/index.d.ts.map +1 -1
- package/dist/sanitize/index.js +91 -32
- package/dist/sanitize/index.js.map +1 -1
- package/dist/server/adapters/bun-adapter.d.ts +1 -1
- package/dist/server/adapters/bun-adapter.d.ts.map +1 -1
- package/dist/server/adapters/bun-adapter.js.map +1 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +5 -2
- package/dist/server/app.js.map +1 -1
- package/dist/server/bootstrapper.d.ts.map +1 -1
- package/dist/server/bootstrapper.js +1 -1
- package/dist/server/bootstrapper.js.map +1 -1
- package/dist/server/helpers/body-parser.d.ts +1 -1
- package/dist/server/helpers/body-parser.d.ts.map +1 -1
- package/dist/server/helpers/body-parser.js +9 -7
- package/dist/server/helpers/body-parser.js.map +1 -1
- package/dist/server/helpers/conditional.d.ts +60 -0
- package/dist/server/helpers/conditional.d.ts.map +1 -0
- package/dist/server/helpers/conditional.js +189 -0
- package/dist/server/helpers/conditional.js.map +1 -0
- package/dist/server/helpers/etag.d.ts +0 -1
- package/dist/server/helpers/etag.d.ts.map +1 -1
- package/dist/server/helpers/etag.js +0 -10
- package/dist/server/helpers/etag.js.map +1 -1
- package/dist/server/helpers/index.d.ts +2 -1
- package/dist/server/helpers/index.d.ts.map +1 -1
- package/dist/server/helpers/index.js +16 -2
- package/dist/server/helpers/index.js.map +1 -1
- package/dist/server/request-handler.d.ts.map +1 -1
- package/dist/server/request-handler.js +12 -2
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts +11 -3
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +41 -83
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +43 -2
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +151 -70
- package/dist/server/response.js.map +1 -1
- package/dist/validator/builder.d.ts +12 -1
- package/dist/validator/builder.d.ts.map +1 -1
- package/dist/validator/builder.js +74 -41
- package/dist/validator/builder.js.map +1 -1
- package/dist/validator/compile.d.ts +7 -2
- package/dist/validator/compile.d.ts.map +1 -1
- package/dist/validator/compile.js +85 -54
- package/dist/validator/compile.js.map +1 -1
- package/dist/validator/engine.d.ts +75 -6
- package/dist/validator/engine.d.ts.map +1 -1
- package/dist/validator/engine.js +483 -540
- package/dist/validator/engine.js.map +1 -1
- package/dist/validator/error.d.ts.map +1 -1
- package/dist/validator/error.js +7 -0
- package/dist/validator/error.js.map +1 -1
- package/dist/validator/rules.d.ts +66 -7
- package/dist/validator/rules.d.ts.map +1 -1
- package/dist/validator/rules.js +435 -109
- package/dist/validator/rules.js.map +1 -1
- package/dist/validator/tex-types.d.ts +15 -0
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +127 -64
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/text.d.ts +2 -0
- package/dist/validator/text.d.ts.map +1 -1
- package/dist/validator/text.js +31 -13
- package/dist/validator/text.js.map +1 -1
- package/dist/validator/types.d.ts +15 -0
- package/dist/validator/types.d.ts.map +1 -1
- package/package.json +3 -5
- package/dist/sanitize/sanitize.d.ts +0 -33
- package/dist/sanitize/sanitize.d.ts.map +0 -1
- package/dist/sanitize/sanitize.js +0 -104
- package/dist/sanitize/sanitize.js.map +0 -1
package/dist/validator/rules.js
CHANGED
|
@@ -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.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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.
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
42
|
-
function
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
454
|
+
field.base = stringCheck(m);
|
|
246
455
|
field.gen = { kind: 'string', m };
|
|
247
456
|
break;
|
|
248
457
|
case 'number':
|
|
249
|
-
field.
|
|
458
|
+
field.base = numberCheck(m);
|
|
250
459
|
field.gen = { kind: 'number', m };
|
|
251
460
|
break;
|
|
252
461
|
case 'boolean':
|
|
253
|
-
field.
|
|
462
|
+
field.base = booleanCheck(m);
|
|
254
463
|
field.gen = { kind: 'boolean', m };
|
|
255
464
|
break;
|
|
256
465
|
case 'email':
|
|
257
|
-
field.
|
|
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.
|
|
262
|
-
field.gen = undefined;
|
|
476
|
+
field.base = passwordCheck(m);
|
|
263
477
|
break;
|
|
264
478
|
case 'uuid':
|
|
265
|
-
field.
|
|
266
|
-
field.gen = undefined;
|
|
479
|
+
field.base = uuidCheck(m.version);
|
|
267
480
|
break;
|
|
268
481
|
case 'cuid':
|
|
269
|
-
field.
|
|
270
|
-
field.gen = undefined;
|
|
482
|
+
field.base = patternCheck(CUID, 'must be a valid CUID');
|
|
271
483
|
break;
|
|
272
484
|
case 'creditcard':
|
|
273
|
-
field.
|
|
274
|
-
field.gen = undefined;
|
|
485
|
+
field.base = creditCardCheck(m);
|
|
275
486
|
break;
|
|
276
|
-
//
|
|
487
|
+
// any; file is validated by the upload middleware
|
|
277
488
|
default:
|
|
278
|
-
field.
|
|
489
|
+
field.base = passThrough;
|
|
490
|
+
field.gen = { kind: 'pass' };
|
|
279
491
|
}
|
|
280
492
|
}
|
|
281
|
-
return field;
|
|
493
|
+
return attachRaw(field, raw);
|
|
282
494
|
}
|
|
283
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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,
|
|
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');
|