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