exisjs 0.9.1 → 0.10.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/bin/exis.js +76 -1
- 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 +16 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/docs/content/README.md +1 -0
- package/dist/docs/content/cli/commands.mdx +23 -1
- package/dist/docs/content/cli/usage.mdx +2 -1
- package/dist/docs/content/config.mdx +50 -4
- package/dist/docs/content/deployment.mdx +163 -0
- package/dist/docs/content/installation.mdx +1 -1
- package/dist/docs/content/introduction.mdx +3 -0
- package/dist/docs/content/validation.mdx +34 -12
- package/dist/docs/reference/config.mdx +3 -1
- package/dist/docs/reference/sanitize.mdx +129 -134
- package/dist/docs/reference/validator.mdx +12 -3
- package/dist/internal.d.ts +16 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +45 -0
- package/dist/internal.js.map +1 -0
- package/dist/{cli → lib}/resolve-aliases.d.ts.map +1 -1
- package/dist/{cli → lib}/resolve-aliases.js.map +1 -1
- package/dist/lib/start-command.d.ts +11 -0
- package/dist/lib/start-command.d.ts.map +1 -0
- package/dist/lib/start-command.js +79 -0
- package/dist/lib/start-command.js.map +1 -0
- package/dist/lib/start-server.js +4 -2
- package/dist/lib/start-server.js.map +1 -1
- package/dist/middleware/backpressure.d.ts +5 -0
- package/dist/middleware/backpressure.d.ts.map +1 -1
- package/dist/middleware/backpressure.js +66 -36
- package/dist/middleware/backpressure.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 +65 -51
- package/dist/router/route-scanner.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 +24 -1
- package/dist/server/app.js.map +1 -1
- package/dist/server/bootstrapper.d.ts.map +1 -1
- package/dist/server/bootstrapper.js +4 -2
- package/dist/server/bootstrapper.js.map +1 -1
- package/dist/server/cluster.d.ts.map +1 -1
- package/dist/server/cluster.js +3 -0
- package/dist/server/cluster.js.map +1 -1
- package/dist/server/metrics.d.ts +43 -0
- package/dist/server/metrics.d.ts.map +1 -0
- package/dist/server/metrics.js +254 -0
- package/dist/server/metrics.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +31 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/validator/builder.d.ts +32 -17
- package/dist/validator/builder.d.ts.map +1 -1
- package/dist/validator/builder.js +84 -25
- 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 +58 -7
- package/dist/validator/engine.d.ts.map +1 -1
- package/dist/validator/engine.js +436 -522
- 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/index.d.ts +4 -1
- package/dist/validator/index.d.ts.map +1 -1
- package/dist/validator/index.js +5 -1
- package/dist/validator/index.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/tex.d.ts +1 -1
- package/dist/validator/tex.d.ts.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 +22 -0
- package/dist/validator/types.d.ts.map +1 -1
- package/package.json +81 -9
- package/dist/cli/commands/build.d.ts +0 -11
- package/dist/cli/commands/build.d.ts.map +0 -1
- package/dist/cli/commands/build.js +0 -344
- package/dist/cli/commands/build.js.map +0 -1
- package/dist/cli/commands/clean.d.ts +0 -5
- package/dist/cli/commands/clean.d.ts.map +0 -1
- package/dist/cli/commands/clean.js +0 -30
- package/dist/cli/commands/clean.js.map +0 -1
- package/dist/cli/commands/dev.d.ts +0 -21
- package/dist/cli/commands/dev.d.ts.map +0 -1
- package/dist/cli/commands/dev.js +0 -842
- package/dist/cli/commands/dev.js.map +0 -1
- package/dist/cli/commands/doctor.d.ts +0 -2
- package/dist/cli/commands/doctor.d.ts.map +0 -1
- package/dist/cli/commands/doctor.js +0 -228
- package/dist/cli/commands/doctor.js.map +0 -1
- package/dist/cli/commands/exports.d.ts +0 -2
- package/dist/cli/commands/exports.d.ts.map +0 -1
- package/dist/cli/commands/exports.js +0 -48
- package/dist/cli/commands/exports.js.map +0 -1
- package/dist/cli/commands/generate.d.ts +0 -33
- package/dist/cli/commands/generate.d.ts.map +0 -1
- package/dist/cli/commands/generate.js +0 -469
- package/dist/cli/commands/generate.js.map +0 -1
- package/dist/cli/commands/init.d.ts +0 -2
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/init.js +0 -37
- package/dist/cli/commands/init.js.map +0 -1
- package/dist/cli/commands/repl.d.ts +0 -6
- package/dist/cli/commands/repl.d.ts.map +0 -1
- package/dist/cli/commands/repl.js +0 -75
- package/dist/cli/commands/repl.js.map +0 -1
- package/dist/cli/commands/run.d.ts +0 -7
- package/dist/cli/commands/run.d.ts.map +0 -1
- package/dist/cli/commands/run.js +0 -101
- package/dist/cli/commands/run.js.map +0 -1
- package/dist/cli/commands/start.d.ts +0 -8
- package/dist/cli/commands/start.d.ts.map +0 -1
- package/dist/cli/commands/start.js +0 -155
- package/dist/cli/commands/start.js.map +0 -1
- package/dist/cli/commands/test.d.ts +0 -8
- package/dist/cli/commands/test.d.ts.map +0 -1
- package/dist/cli/commands/test.js +0 -165
- package/dist/cli/commands/test.js.map +0 -1
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -268
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/manifest.d.ts +0 -4
- package/dist/cli/manifest.d.ts.map +0 -1
- package/dist/cli/manifest.js +0 -489
- package/dist/cli/manifest.js.map +0 -1
- package/dist/cli/routes.d.ts +0 -17
- package/dist/cli/routes.d.ts.map +0 -1
- package/dist/cli/routes.js +0 -317
- package/dist/cli/routes.js.map +0 -1
- package/dist/cli/utils.d.ts +0 -24
- package/dist/cli/utils.d.ts.map +0 -1
- package/dist/cli/utils.js +0 -54
- package/dist/cli/utils.js.map +0 -1
- package/dist/lib/inspect-routes.d.ts +0 -2
- package/dist/lib/inspect-routes.d.ts.map +0 -1
- package/dist/lib/inspect-routes.js +0 -150
- package/dist/lib/inspect-routes.js.map +0 -1
- package/dist/lib/start-repl.d.ts +0 -2
- package/dist/lib/start-repl.d.ts.map +0 -1
- package/dist/lib/start-repl.js +0 -120
- package/dist/lib/start-repl.js.map +0 -1
- 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/{cli → lib}/resolve-aliases.d.ts +0 -0
- /package/dist/{cli → lib}/resolve-aliases.js +0 -0
package/dist/validator/engine.js
CHANGED
|
@@ -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
|
|
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
|
|
43
|
-
*
|
|
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
|
|
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
|
|
209
|
+
return firstErrors;
|
|
49
210
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
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 (
|
|
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
|
-
|
|
73
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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();
|
|
@@ -217,13 +309,18 @@ class TexEngine {
|
|
|
217
309
|
newSchema[key] = `${val} | optional`;
|
|
218
310
|
const rawVal = this.rawSchema[key];
|
|
219
311
|
if (rawVal instanceof tex_types_1.TexType) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
312
|
+
// A field left out of a partial update stays unset: a default would
|
|
313
|
+
// overwrite the stored value (PATCH without `tags` -> tags: [])
|
|
314
|
+
const field = rawVal.optional();
|
|
315
|
+
field.defaultValue = undefined;
|
|
316
|
+
newRaw[key] = field;
|
|
223
317
|
}
|
|
224
318
|
else if (rawVal instanceof TexEngine) {
|
|
225
319
|
newRaw[key] = rawVal.optional();
|
|
226
320
|
}
|
|
321
|
+
else if (rawVal !== null && typeof rawVal === 'object') {
|
|
322
|
+
newRaw[key] = rawVal;
|
|
323
|
+
}
|
|
227
324
|
else {
|
|
228
325
|
newRaw[key] = newSchema[key];
|
|
229
326
|
}
|
|
@@ -269,22 +366,24 @@ class TexEngine {
|
|
|
269
366
|
}
|
|
270
367
|
return this.derive(newSchema, newRaw);
|
|
271
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Validates `data` and returns a new object with the declared fields only.
|
|
371
|
+
* The input is never modified. Throws `ValidatorError` listing every
|
|
372
|
+
* failing field.
|
|
373
|
+
*/
|
|
272
374
|
parse(data) {
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
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;
|
|
375
|
+
const query = Boolean(this._isQueryOrParam);
|
|
376
|
+
const validate = query
|
|
377
|
+
? (this._queryValidator ?? (this._queryValidator = (0, compile_1.compileSchema)(this.schema, this.strict, this.rawSchema, true)))
|
|
378
|
+
: this.validator;
|
|
284
379
|
try {
|
|
285
|
-
|
|
380
|
+
const result = validate(data);
|
|
381
|
+
return this._objectCheck ? this._objectCheck(result) : result;
|
|
286
382
|
}
|
|
287
383
|
catch (err) {
|
|
384
|
+
if (!(err instanceof rules_1.RuleError) && !(err instanceof error_1.ValidatorError)) {
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
288
387
|
if (data === process.env &&
|
|
289
388
|
(process.env.__EXIS_SKIP_ENV_CHECK === 'true' ||
|
|
290
389
|
process.env.EXIS_SKIP_ENV_CHECK === 'true')) {
|
|
@@ -304,77 +403,24 @@ class TexEngine {
|
|
|
304
403
|
}
|
|
305
404
|
return mock;
|
|
306
405
|
}
|
|
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
|
-
}
|
|
406
|
+
throw new error_1.ValidatorError(this.collectErrors(err, data, query));
|
|
341
407
|
}
|
|
342
|
-
return parsedData;
|
|
343
408
|
}
|
|
409
|
+
/** Like `parse()`, but returns `{ success, data }` or `{ success, errors }`. */
|
|
410
|
+
safeParse(data) {
|
|
411
|
+
return safely(() => this.parse(data));
|
|
412
|
+
}
|
|
413
|
+
/** Runs `parse()`, then every `refineAsync()` rule, nested ones included. */
|
|
344
414
|
async parseAsync(data) {
|
|
345
|
-
const parsed = this.parse(data);
|
|
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
|
-
}
|
|
415
|
+
const parsed = this.parse(data);
|
|
416
|
+
const errors = await runAsyncRefinements(this, parsed);
|
|
374
417
|
if (errors.length > 0)
|
|
375
418
|
throw new error_1.ValidatorError(errors);
|
|
376
419
|
return parsed;
|
|
377
420
|
}
|
|
421
|
+
safeParseAsync(data) {
|
|
422
|
+
return safelyAsync(() => this.parseAsync(data));
|
|
423
|
+
}
|
|
378
424
|
toMongoSchema() {
|
|
379
425
|
const schemaDef = {};
|
|
380
426
|
for (const [key, rawDef] of Object.entries(this.schema)) {
|
|
@@ -384,6 +430,7 @@ class TexEngine {
|
|
|
384
430
|
const field = {};
|
|
385
431
|
if (baseType.startsWith('string') ||
|
|
386
432
|
baseType.startsWith('email') ||
|
|
433
|
+
baseType.startsWith('url') ||
|
|
387
434
|
baseType.startsWith('uuid') ||
|
|
388
435
|
baseType.startsWith('cuid') ||
|
|
389
436
|
baseType.startsWith('password')) {
|
|
@@ -414,15 +461,16 @@ class TexEngine {
|
|
|
414
461
|
const rawDef = this.schema[key] || '';
|
|
415
462
|
const typeStr = rawDef.split('|')[0].trim();
|
|
416
463
|
// A nested rule's text includes its children's modifiers
|
|
417
|
-
const
|
|
418
|
-
|
|
464
|
+
const engine = rawVal instanceof TexTransformedEngine ? rawVal.inner : rawVal;
|
|
465
|
+
const isOptional = engine instanceof TexEngine
|
|
466
|
+
? engine._isOptional || / \| optional$/.test(rawDef)
|
|
419
467
|
: typeStr.endsWith('?') || rawDef.includes('optional');
|
|
420
468
|
let prop;
|
|
421
469
|
if (rawVal && typeof rawVal.toOpenApi === 'function') {
|
|
422
470
|
prop = rawVal.toOpenApi();
|
|
423
471
|
}
|
|
424
472
|
else {
|
|
425
|
-
const baseType =
|
|
473
|
+
const baseType = (0, rules_1.ruleBase)(rawDef);
|
|
426
474
|
prop = {};
|
|
427
475
|
if (baseType.startsWith('string') ||
|
|
428
476
|
baseType.startsWith('uuid') ||
|
|
@@ -434,6 +482,10 @@ class TexEngine {
|
|
|
434
482
|
prop.type = 'string';
|
|
435
483
|
prop.format = 'email';
|
|
436
484
|
}
|
|
485
|
+
else if (baseType.startsWith('url')) {
|
|
486
|
+
prop.type = 'string';
|
|
487
|
+
prop.format = 'uri';
|
|
488
|
+
}
|
|
437
489
|
else if (baseType.startsWith('number')) {
|
|
438
490
|
prop.type = 'number';
|
|
439
491
|
}
|
|
@@ -449,7 +501,7 @@ class TexEngine {
|
|
|
449
501
|
}
|
|
450
502
|
else if (baseType.startsWith('enum:')) {
|
|
451
503
|
prop.type = 'string';
|
|
452
|
-
prop.enum =
|
|
504
|
+
prop.enum = (0, rules_1.parseEnumValues)(baseType.slice(5));
|
|
453
505
|
}
|
|
454
506
|
else {
|
|
455
507
|
prop.type = 'object';
|
|
@@ -462,6 +514,8 @@ class TexEngine {
|
|
|
462
514
|
const result = { type: 'object', properties };
|
|
463
515
|
if (required.length > 0)
|
|
464
516
|
result.required = required;
|
|
517
|
+
if (this.strict)
|
|
518
|
+
result.additionalProperties = false;
|
|
465
519
|
return result;
|
|
466
520
|
}
|
|
467
521
|
}
|
|
@@ -475,10 +529,16 @@ class TexTransformedEngine {
|
|
|
475
529
|
const validated = this.inner.parse(data);
|
|
476
530
|
return this.transformFn(validated);
|
|
477
531
|
}
|
|
532
|
+
safeParse(data) {
|
|
533
|
+
return safely(() => this.parse(data));
|
|
534
|
+
}
|
|
478
535
|
async parseAsync(data) {
|
|
479
536
|
const validated = await this.inner.parseAsync(data);
|
|
480
537
|
return this.transformFn(validated);
|
|
481
538
|
}
|
|
539
|
+
safeParseAsync(data) {
|
|
540
|
+
return safelyAsync(() => this.parseAsync(data));
|
|
541
|
+
}
|
|
482
542
|
toOpenApi() {
|
|
483
543
|
return this.inner.toOpenApi();
|
|
484
544
|
}
|
|
@@ -490,14 +550,30 @@ class TexDiscriminatedUnionEngine {
|
|
|
490
550
|
this.variants = variants;
|
|
491
551
|
this.engines = variants.map((v) => v instanceof TexEngine
|
|
492
552
|
? v
|
|
493
|
-
: new TexEngine(Object.fromEntries(Object.entries(v).map(([k, val]) => [
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
553
|
+
: new TexEngine(Object.fromEntries(Object.entries(v).map(([k, val]) => [k, fieldRule(k, val)])), v));
|
|
554
|
+
this.tags = this.engines.map((engine) => {
|
|
555
|
+
const rule = engine.getCompiledSchema()[discriminator];
|
|
556
|
+
if (rule === undefined)
|
|
557
|
+
return [];
|
|
558
|
+
const base = (0, rules_1.ruleBase)(rule);
|
|
559
|
+
if (base.startsWith('literal:'))
|
|
560
|
+
return [(0, rules_1.parseLiteral)(base.slice(8))];
|
|
561
|
+
if (base.startsWith('enum:'))
|
|
562
|
+
return (0, rules_1.parseEnumValues)(base.slice(5));
|
|
563
|
+
return [];
|
|
564
|
+
});
|
|
497
565
|
}
|
|
498
|
-
|
|
566
|
+
/** The variant whose discriminator matches `data`, if any */
|
|
567
|
+
variantFor(data) {
|
|
568
|
+
if (!data || typeof data !== 'object')
|
|
569
|
+
return undefined;
|
|
570
|
+
const tag = data[this.discriminator];
|
|
571
|
+
const i = this.tags.findIndex((tags) => tags.includes(tag));
|
|
572
|
+
return i === -1 ? undefined : this.engines[i];
|
|
573
|
+
}
|
|
574
|
+
noMatch(data) {
|
|
499
575
|
if (!data || typeof data !== 'object') {
|
|
500
|
-
|
|
576
|
+
return new error_1.ValidatorError([
|
|
501
577
|
{
|
|
502
578
|
path: '',
|
|
503
579
|
message: 'Expected an object for discriminated union payload',
|
|
@@ -509,7 +585,7 @@ class TexDiscriminatedUnionEngine {
|
|
|
509
585
|
}
|
|
510
586
|
const tagValue = data[this.discriminator];
|
|
511
587
|
if (tagValue === undefined) {
|
|
512
|
-
|
|
588
|
+
return new error_1.ValidatorError([
|
|
513
589
|
{
|
|
514
590
|
path: this.discriminator,
|
|
515
591
|
message: `Missing discriminator field '${this.discriminator}'`,
|
|
@@ -519,83 +595,33 @@ class TexDiscriminatedUnionEngine {
|
|
|
519
595
|
},
|
|
520
596
|
]);
|
|
521
597
|
}
|
|
522
|
-
|
|
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([
|
|
598
|
+
return new error_1.ValidatorError([
|
|
555
599
|
{
|
|
556
600
|
path: this.discriminator,
|
|
557
601
|
message: `Invalid discriminator value '${tagValue}' for field '${this.discriminator}'`,
|
|
558
|
-
expected:
|
|
602
|
+
expected: `one of ${JSON.stringify(this.tags.flat())}`,
|
|
559
603
|
received: (0, error_1.formatReceivedValue)(tagValue),
|
|
560
604
|
code: 'INVALID_DISCRIMINATOR_VALUE',
|
|
561
605
|
},
|
|
562
606
|
]);
|
|
563
607
|
}
|
|
608
|
+
parse(data) {
|
|
609
|
+
const engine = this.variantFor(data);
|
|
610
|
+
if (!engine)
|
|
611
|
+
throw this.noMatch(data);
|
|
612
|
+
return engine.parse(data);
|
|
613
|
+
}
|
|
614
|
+
safeParse(data) {
|
|
615
|
+
return safely(() => this.parse(data));
|
|
616
|
+
}
|
|
564
617
|
async parseAsync(data) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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);
|
|
618
|
+
const engine = this.variantFor(data);
|
|
619
|
+
if (!engine)
|
|
620
|
+
throw this.noMatch(data);
|
|
621
|
+
return engine.parseAsync(data);
|
|
622
|
+
}
|
|
623
|
+
safeParseAsync(data) {
|
|
624
|
+
return safelyAsync(() => this.parseAsync(data));
|
|
599
625
|
}
|
|
600
626
|
toOpenApi() {
|
|
601
627
|
const oneOf = this.engines.map((e) => e.toOpenApi());
|
|
@@ -608,259 +634,147 @@ class TexDiscriminatedUnionEngine {
|
|
|
608
634
|
}
|
|
609
635
|
}
|
|
610
636
|
exports.TexDiscriminatedUnionEngine = TexDiscriminatedUnionEngine;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
(
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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';
|
|
637
|
+
// ─── Error collection ────────────────────────────────────────────────────────
|
|
638
|
+
function errorsOf(validate, data) {
|
|
639
|
+
try {
|
|
640
|
+
validate(data);
|
|
641
|
+
return [];
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
if (err instanceof error_1.ValidatorError)
|
|
645
|
+
return [...err.errors];
|
|
646
|
+
if (err instanceof rules_1.RuleError)
|
|
647
|
+
return [describeRuleError(err, data)];
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
646
650
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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),
|
|
651
|
+
// Validates one item of an array (or one value of a record) at a time
|
|
652
|
+
const itemValidators = new WeakMap();
|
|
653
|
+
function itemValidator(container, isArray) {
|
|
654
|
+
let validate = itemValidators.get(container);
|
|
655
|
+
if (!validate) {
|
|
656
|
+
const item = isArray ? container._item : container._value;
|
|
657
|
+
const coerce = isArray && (0, rules_1.ruleParts)(container._raw).includes('coerce');
|
|
658
|
+
const rule = isArray
|
|
659
|
+
? `array<${fieldRule('[item]', item)}>${coerce ? ' | coerce' : ''}`
|
|
660
|
+
: `record<${fieldRule('[value]', item)}>`;
|
|
661
|
+
validate = (0, compile_1.compileSchema)({ v: rule }, false, {
|
|
662
|
+
v: isArray ? { _item: item } : { _value: item },
|
|
686
663
|
});
|
|
664
|
+
itemValidators.set(container, validate);
|
|
687
665
|
}
|
|
688
|
-
return
|
|
666
|
+
return validate;
|
|
689
667
|
}
|
|
690
|
-
function
|
|
691
|
-
return
|
|
668
|
+
function relabel(errors, from, to) {
|
|
669
|
+
return errors.map((e) => ({
|
|
670
|
+
...e,
|
|
671
|
+
path: e.path.startsWith(from) ? to + e.path.slice(from.length) : to,
|
|
672
|
+
}));
|
|
692
673
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
674
|
+
/** Every failing value inside a nested object, array or record at `path` */
|
|
675
|
+
function collectInner(raw, value, path) {
|
|
676
|
+
if (value === null || typeof value !== 'object')
|
|
677
|
+
return [];
|
|
678
|
+
const engine = raw instanceof TexEngine
|
|
679
|
+
? raw
|
|
680
|
+
: raw instanceof TexTransformedEngine
|
|
681
|
+
? raw.inner
|
|
682
|
+
: undefined;
|
|
683
|
+
if (engine) {
|
|
684
|
+
if (Array.isArray(value))
|
|
685
|
+
return [];
|
|
686
|
+
return engine._collectFields(value).map((e) => ({
|
|
687
|
+
...e,
|
|
688
|
+
path: e.path === 'root' ? path : `${path}.${e.path}`,
|
|
689
|
+
}));
|
|
690
|
+
}
|
|
691
|
+
if (!(raw instanceof tex_types_1.TexType))
|
|
692
|
+
return [];
|
|
693
|
+
const isArray = Array.isArray(value);
|
|
694
|
+
const item = isArray ? raw._item : raw._value;
|
|
695
|
+
if (item === undefined)
|
|
696
|
+
return [];
|
|
697
|
+
const validate = itemValidator(raw, isArray);
|
|
698
|
+
const keys = isArray
|
|
699
|
+
? value.map((_, i) => i)
|
|
700
|
+
: Object.keys(value).filter((k) => k !== '__proto__' && k !== 'constructor');
|
|
701
|
+
const out = [];
|
|
702
|
+
for (const k of keys) {
|
|
703
|
+
if (out.length >= MAX_ERRORS)
|
|
704
|
+
break;
|
|
705
|
+
const single = isArray ? [value[k]] : { [k]: value[k] };
|
|
706
|
+
const at = isArray ? `v[0]` : `v.${k}`;
|
|
707
|
+
const itemPath = isArray ? `${path}[${k}]` : `${path}.${k}`;
|
|
708
|
+
const found = errorsOf(validate, { v: single });
|
|
709
|
+
if (found.length === 0)
|
|
710
|
+
continue;
|
|
711
|
+
const nested = found.some((e) => e.path !== at)
|
|
712
|
+
? collectInner(item, value[k], itemPath)
|
|
713
|
+
: [];
|
|
714
|
+
out.push(...(nested.length > 0 ? nested : relabel(found, at, itemPath)));
|
|
727
715
|
}
|
|
728
|
-
return
|
|
716
|
+
return out;
|
|
729
717
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
|
|
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];
|
|
718
|
+
// ─── Async refinements ───────────────────────────────────────────────────────
|
|
719
|
+
// Walks the parsed value next to its schema, so `refineAsync()` rules run at
|
|
720
|
+
// every depth: nested objects, array items and record values.
|
|
721
|
+
function joinPath(path, key) {
|
|
722
|
+
if (typeof key === 'number')
|
|
723
|
+
return `${path}[${key}]`;
|
|
724
|
+
return path ? `${path}.${key}` : key;
|
|
725
|
+
}
|
|
726
|
+
function collectAsync(node, value, path, tasks) {
|
|
751
727
|
if (value === undefined || value === null)
|
|
752
728
|
return;
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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');
|
|
729
|
+
if (node instanceof TexEngine) {
|
|
730
|
+
if (typeof value !== 'object')
|
|
731
|
+
return;
|
|
732
|
+
const raw = node.rawSchema;
|
|
733
|
+
for (const key of Object.keys(raw)) {
|
|
734
|
+
collectAsync(raw[key], value[key], joinPath(path, key), tasks);
|
|
776
735
|
}
|
|
736
|
+
return;
|
|
777
737
|
}
|
|
778
|
-
if (
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
break;
|
|
784
|
-
data[key] = s(data[key]);
|
|
785
|
-
}
|
|
738
|
+
if (node instanceof TexDiscriminatedUnionEngine) {
|
|
739
|
+
const engine = node.variantFor(value);
|
|
740
|
+
if (engine)
|
|
741
|
+
collectAsync(engine, value, path, tasks);
|
|
742
|
+
return;
|
|
786
743
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
744
|
+
if (!(node instanceof tex_types_1.TexType))
|
|
745
|
+
return;
|
|
746
|
+
for (const r of node.refinements) {
|
|
747
|
+
if (!r.async)
|
|
748
|
+
continue;
|
|
749
|
+
tasks.push(Promise.resolve(r.fn(value)).then((ok) => ok
|
|
750
|
+
? null
|
|
751
|
+
: {
|
|
752
|
+
path: path || 'root',
|
|
753
|
+
message: (0, rules_1.refinementMessage)(r, value),
|
|
754
|
+
expected: 'custom async refinement rule',
|
|
755
|
+
received: (0, error_1.formatReceivedValue)(value),
|
|
756
|
+
code: 'CUSTOM_VALIDATION',
|
|
757
|
+
}));
|
|
797
758
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
}
|
|
759
|
+
const item = node._item;
|
|
760
|
+
if (item && Array.isArray(value)) {
|
|
761
|
+
for (let i = 0; i < value.length; i++) {
|
|
762
|
+
collectAsync(item, value[i], joinPath(path, i), tasks);
|
|
843
763
|
}
|
|
844
764
|
}
|
|
845
|
-
const
|
|
846
|
-
if (
|
|
847
|
-
|
|
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
|
-
}
|
|
765
|
+
const recordValue = node._value;
|
|
766
|
+
if (recordValue && typeof value === 'object') {
|
|
767
|
+
for (const key of Object.keys(value)) {
|
|
768
|
+
collectAsync(recordValue, value[key], joinPath(path, key), tasks);
|
|
863
769
|
}
|
|
864
770
|
}
|
|
865
771
|
}
|
|
772
|
+
async function runAsyncRefinements(engine, parsed) {
|
|
773
|
+
const tasks = [];
|
|
774
|
+
collectAsync(engine, parsed, '', tasks);
|
|
775
|
+
if (tasks.length === 0)
|
|
776
|
+
return [];
|
|
777
|
+
const results = await Promise.all(tasks);
|
|
778
|
+
return results.filter((e) => e !== null);
|
|
779
|
+
}
|
|
866
780
|
//# sourceMappingURL=engine.js.map
|