@foormjs/atscript 0.2.0 → 0.2.1
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 +34 -1
- package/dist/index.cjs +576 -88
- package/dist/index.d.ts +5 -2
- package/dist/index.mjs +576 -88
- package/dist/plugin.cjs +49 -0
- package/dist/plugin.mjs +49 -0
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -35,19 +35,19 @@ function compileTopFn(fnStr) {
|
|
|
35
35
|
* Compiles a validator function string from a @foorm.validate annotation.
|
|
36
36
|
*
|
|
37
37
|
* The function string should be:
|
|
38
|
-
* "(v, data, ctx) => boolean | string"
|
|
38
|
+
* "(v, data, ctx, entry) => boolean | string"
|
|
39
39
|
*
|
|
40
40
|
* The compiled function receives a single TFoormFnScope object:
|
|
41
|
-
* { v, data, context }
|
|
41
|
+
* { v, data, context, entry }
|
|
42
42
|
*/
|
|
43
43
|
function compileValidatorFn(fnStr) {
|
|
44
|
-
const code = `return (${fnStr})(v, data, context)`;
|
|
44
|
+
const code = `return (${fnStr})(v, data, context, entry)`;
|
|
45
45
|
return pool.getFn(code);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function foormValidatorPlugin(foormCtx) {
|
|
49
49
|
return (ctx, def, value) => {
|
|
50
|
-
var _a, _b, _c;
|
|
50
|
+
var _a, _b, _c, _d, _e;
|
|
51
51
|
const validators = (_a = def.metadata) === null || _a === void 0 ? void 0 : _a.get('foorm.validate');
|
|
52
52
|
if (!validators) {
|
|
53
53
|
return undefined;
|
|
@@ -55,12 +55,31 @@ function foormValidatorPlugin(foormCtx) {
|
|
|
55
55
|
const fns = Array.isArray(validators) ? validators : [validators];
|
|
56
56
|
const data = (_b = foormCtx === null || foormCtx === void 0 ? void 0 : foormCtx.data) !== null && _b !== void 0 ? _b : {};
|
|
57
57
|
const context = (_c = foormCtx === null || foormCtx === void 0 ? void 0 : foormCtx.context) !== null && _c !== void 0 ? _c : {};
|
|
58
|
+
// Build entry object with field metadata
|
|
59
|
+
const entry = {
|
|
60
|
+
field: def.name,
|
|
61
|
+
type: ((_d = def.metadata) === null || _d === void 0 ? void 0 : _d.get('foorm.type')) || 'text',
|
|
62
|
+
component: (_e = def.metadata) === null || _e === void 0 ? void 0 : _e.get('foorm.component'),
|
|
63
|
+
name: def.name,
|
|
64
|
+
};
|
|
65
|
+
// Base scope for evaluating constraints
|
|
66
|
+
const baseScope = { v: value, data, context, entry: undefined };
|
|
67
|
+
// Evaluate computed constraints
|
|
68
|
+
entry.disabled = evalConstraint(def.metadata, 'foorm.disabled', 'foorm.fn.disabled', baseScope);
|
|
69
|
+
entry.optional = evalConstraint(def.metadata, 'foorm.optional', 'foorm.fn.optional', baseScope);
|
|
70
|
+
entry.hidden = evalConstraint(def.metadata, 'foorm.hidden', 'foorm.fn.hidden', baseScope);
|
|
71
|
+
entry.readonly = evalConstraint(def.metadata, 'foorm.readonly', 'foorm.fn.readonly', baseScope);
|
|
72
|
+
// Full scope with evaluated entry
|
|
73
|
+
const scope = { v: value, data, context, entry };
|
|
74
|
+
// Evaluate options (static or computed)
|
|
75
|
+
entry.options = evalOptions(def.metadata, scope);
|
|
76
|
+
// Run custom validators with full scope
|
|
58
77
|
for (const fnStr of fns) {
|
|
59
78
|
if (typeof fnStr !== 'string') {
|
|
60
79
|
continue;
|
|
61
80
|
}
|
|
62
81
|
const fn = compileValidatorFn(fnStr);
|
|
63
|
-
const result = fn(
|
|
82
|
+
const result = fn(scope);
|
|
64
83
|
if (result !== true) {
|
|
65
84
|
ctx.error(typeof result === 'string' ? result : 'Validation failed');
|
|
66
85
|
return false;
|
|
@@ -69,6 +88,429 @@ function foormValidatorPlugin(foormCtx) {
|
|
|
69
88
|
return undefined;
|
|
70
89
|
};
|
|
71
90
|
}
|
|
91
|
+
/** Helper to evaluate a boolean constraint (static or computed) */
|
|
92
|
+
function evalConstraint(metadata, staticKey, fnKey, scope) {
|
|
93
|
+
const fnStr = metadata === null || metadata === void 0 ? void 0 : metadata.get(fnKey);
|
|
94
|
+
if (typeof fnStr === 'string') {
|
|
95
|
+
return compileFieldFn(fnStr)(scope);
|
|
96
|
+
}
|
|
97
|
+
const staticVal = metadata === null || metadata === void 0 ? void 0 : metadata.get(staticKey);
|
|
98
|
+
return staticVal !== undefined ? true : undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Helper to evaluate options (static or computed) */
|
|
101
|
+
function evalOptions(metadata, scope) {
|
|
102
|
+
const fnStr = metadata === null || metadata === void 0 ? void 0 : metadata.get('foorm.fn.options');
|
|
103
|
+
if (typeof fnStr === 'string') {
|
|
104
|
+
return compileFieldFn(fnStr)(scope);
|
|
105
|
+
}
|
|
106
|
+
const staticOpts = metadata === null || metadata === void 0 ? void 0 : metadata.get('foorm.options');
|
|
107
|
+
if (staticOpts) {
|
|
108
|
+
const items = Array.isArray(staticOpts) ? staticOpts : [staticOpts];
|
|
109
|
+
return items.map(item => {
|
|
110
|
+
if (typeof item === 'object' && item !== null && 'label' in item) {
|
|
111
|
+
const { label, value } = item;
|
|
112
|
+
return value !== undefined ? { key: value, label } : label;
|
|
113
|
+
}
|
|
114
|
+
return String(item);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//#region packages/typescript/src/traverse.ts
|
|
121
|
+
function forAnnotatedType(def, handlers) {
|
|
122
|
+
switch (def.type.kind) {
|
|
123
|
+
case "": {
|
|
124
|
+
const typed = def;
|
|
125
|
+
if (handlers.phantom && typed.type.designType === "phantom") return handlers.phantom(typed);
|
|
126
|
+
return handlers.final(typed);
|
|
127
|
+
}
|
|
128
|
+
case "object": return handlers.object(def);
|
|
129
|
+
case "array": return handlers.array(def);
|
|
130
|
+
case "union": return handlers.union(def);
|
|
131
|
+
case "intersection": return handlers.intersection(def);
|
|
132
|
+
case "tuple": return handlers.tuple(def);
|
|
133
|
+
default: throw new Error(`Unknown type kind "${def.type.kind}"`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region packages/typescript/src/validator.ts
|
|
139
|
+
function _define_property(obj, key, value) {
|
|
140
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
141
|
+
value,
|
|
142
|
+
enumerable: true,
|
|
143
|
+
configurable: true,
|
|
144
|
+
writable: true
|
|
145
|
+
});
|
|
146
|
+
else obj[key] = value;
|
|
147
|
+
return obj;
|
|
148
|
+
}
|
|
149
|
+
const regexCache = new Map();
|
|
150
|
+
var Validator = class {
|
|
151
|
+
isLimitExceeded() {
|
|
152
|
+
if (this.stackErrors.length > 0) return this.stackErrors[this.stackErrors.length - 1].length >= this.opts.errorLimit;
|
|
153
|
+
return this.errors.length >= this.opts.errorLimit;
|
|
154
|
+
}
|
|
155
|
+
push(name) {
|
|
156
|
+
this.stackPath.push(name);
|
|
157
|
+
this.stackErrors.push([]);
|
|
158
|
+
}
|
|
159
|
+
pop(saveErrors) {
|
|
160
|
+
this.stackPath.pop();
|
|
161
|
+
const popped = this.stackErrors.pop();
|
|
162
|
+
if (saveErrors && popped?.length) popped.forEach((error) => {
|
|
163
|
+
this.error(error.message, error.path, error.details);
|
|
164
|
+
});
|
|
165
|
+
return popped;
|
|
166
|
+
}
|
|
167
|
+
clear() {
|
|
168
|
+
this.stackErrors[this.stackErrors.length - 1] = [];
|
|
169
|
+
}
|
|
170
|
+
error(message, path, details) {
|
|
171
|
+
const errors = this.stackErrors[this.stackErrors.length - 1] || this.errors;
|
|
172
|
+
const error = {
|
|
173
|
+
path: path || this.path,
|
|
174
|
+
message
|
|
175
|
+
};
|
|
176
|
+
if (details?.length) error.details = details;
|
|
177
|
+
errors.push(error);
|
|
178
|
+
}
|
|
179
|
+
throw() {
|
|
180
|
+
throw new ValidatorError(this.errors);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Validates a value against the type definition.
|
|
184
|
+
*
|
|
185
|
+
* Acts as a TypeScript type guard — when it returns `true`, the value
|
|
186
|
+
* is narrowed to `DataType`.
|
|
187
|
+
*
|
|
188
|
+
* @param value - The value to validate.
|
|
189
|
+
* @param safe - If `true`, returns `false` on failure instead of throwing.
|
|
190
|
+
* @returns `true` if the value matches the type definition.
|
|
191
|
+
* @throws {ValidatorError} When validation fails and `safe` is not `true`.
|
|
192
|
+
*/ validate(value, safe) {
|
|
193
|
+
this.push("");
|
|
194
|
+
this.errors = [];
|
|
195
|
+
this.stackErrors = [];
|
|
196
|
+
const passed = this.validateSafe(this.def, value);
|
|
197
|
+
this.pop(!passed);
|
|
198
|
+
if (!passed) {
|
|
199
|
+
if (safe) return false;
|
|
200
|
+
this.throw();
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
validateSafe(def, value) {
|
|
205
|
+
if (this.isLimitExceeded()) return false;
|
|
206
|
+
if (!isAnnotatedType(def)) throw new Error("Can not validate not-annotated type");
|
|
207
|
+
if (typeof this.opts.replace === "function") def = this.opts.replace(def, this.path);
|
|
208
|
+
if (def.optional && value === undefined) return true;
|
|
209
|
+
for (const plugin of this.opts.plugins) {
|
|
210
|
+
const result = plugin(this, def, value);
|
|
211
|
+
if (result === false || result === true) return result;
|
|
212
|
+
}
|
|
213
|
+
return this.validateAnnotatedType(def, value);
|
|
214
|
+
}
|
|
215
|
+
get path() {
|
|
216
|
+
return this.stackPath.slice(1).join(".");
|
|
217
|
+
}
|
|
218
|
+
validateAnnotatedType(def, value) {
|
|
219
|
+
return forAnnotatedType(def, {
|
|
220
|
+
final: (d) => this.validatePrimitive(d, value),
|
|
221
|
+
phantom: () => true,
|
|
222
|
+
object: (d) => this.validateObject(d, value),
|
|
223
|
+
array: (d) => this.validateArray(d, value),
|
|
224
|
+
union: (d) => this.validateUnion(d, value),
|
|
225
|
+
intersection: (d) => this.validateIntersection(d, value),
|
|
226
|
+
tuple: (d) => this.validateTuple(d, value)
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
validateUnion(def, value) {
|
|
230
|
+
let i = 0;
|
|
231
|
+
const popped = [];
|
|
232
|
+
for (const item of def.type.items) {
|
|
233
|
+
this.push(`[${item.type.kind || item.type.designType}(${i})]`);
|
|
234
|
+
if (this.validateSafe(item, value)) {
|
|
235
|
+
this.pop(false);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
const errors = this.pop(false);
|
|
239
|
+
if (errors) popped.push(...errors);
|
|
240
|
+
i++;
|
|
241
|
+
}
|
|
242
|
+
this.clear();
|
|
243
|
+
const expected = def.type.items.map((item, i$1) => `[${item.type.kind || item.type.designType}(${i$1})]`).join(", ");
|
|
244
|
+
this.error(`Value does not match any of the allowed types: ${expected}`, undefined, popped);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
validateIntersection(def, value) {
|
|
248
|
+
for (const item of def.type.items) if (!this.validateSafe(item, value)) return false;
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
validateTuple(def, value) {
|
|
252
|
+
if (!Array.isArray(value) || value.length !== def.type.items.length) {
|
|
253
|
+
this.error(`Expected array of length ${def.type.items.length}`);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
let i = 0;
|
|
257
|
+
for (const item of def.type.items) {
|
|
258
|
+
this.push(`[${i}]`);
|
|
259
|
+
if (!this.validateSafe(item, value[i])) {
|
|
260
|
+
this.pop(true);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
this.pop(false);
|
|
264
|
+
i++;
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
validateArray(def, value) {
|
|
269
|
+
if (!Array.isArray(value)) {
|
|
270
|
+
this.error("Expected array");
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
const minLength = def.metadata.get("expect.minLength");
|
|
274
|
+
if (minLength) {
|
|
275
|
+
const length = typeof minLength === "number" ? minLength : minLength.length;
|
|
276
|
+
if (value.length < length) {
|
|
277
|
+
const message = typeof minLength === "object" && minLength.message ? minLength.message : `Expected minimum length of ${length} items, got ${value.length} items`;
|
|
278
|
+
this.error(message);
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const maxLength = def.metadata.get("expect.maxLength");
|
|
283
|
+
if (maxLength) {
|
|
284
|
+
const length = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
285
|
+
if (value.length > length) {
|
|
286
|
+
const message = typeof maxLength === "object" && maxLength.message ? maxLength.message : `Expected maximum length of ${length} items, got ${value.length} items`;
|
|
287
|
+
this.error(message);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
let i = 0;
|
|
292
|
+
let passed = true;
|
|
293
|
+
for (const item of value) {
|
|
294
|
+
this.push(`[${i}]`);
|
|
295
|
+
if (!this.validateSafe(def.type.of, item)) {
|
|
296
|
+
passed = false;
|
|
297
|
+
this.pop(true);
|
|
298
|
+
if (this.isLimitExceeded()) return false;
|
|
299
|
+
} else this.pop(false);
|
|
300
|
+
i++;
|
|
301
|
+
}
|
|
302
|
+
return passed;
|
|
303
|
+
}
|
|
304
|
+
validateObject(def, value) {
|
|
305
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
306
|
+
this.error("Expected object");
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
let passed = true;
|
|
310
|
+
const valueKeys = new Set(Object.keys(value));
|
|
311
|
+
const typeKeys = new Set();
|
|
312
|
+
const skipList = new Set();
|
|
313
|
+
if (this.opts.skipList) {
|
|
314
|
+
const path = this.stackPath.length > 1 ? `${this.path}.` : "";
|
|
315
|
+
this.opts.skipList.forEach((item) => {
|
|
316
|
+
if (item.startsWith(path)) {
|
|
317
|
+
const key = item.slice(path.length);
|
|
318
|
+
skipList.add(key);
|
|
319
|
+
valueKeys.delete(key);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
let partialFunctionMatched = false;
|
|
324
|
+
if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.path);
|
|
325
|
+
for (const [key, item] of def.type.props.entries()) {
|
|
326
|
+
if (skipList.has(key) || isPhantomType(item)) continue;
|
|
327
|
+
typeKeys.add(key);
|
|
328
|
+
if (value[key] === undefined) {
|
|
329
|
+
if (partialFunctionMatched || this.opts.partial === "deep" || this.opts.partial === true && this.stackPath.length <= 1) continue;
|
|
330
|
+
}
|
|
331
|
+
this.push(key);
|
|
332
|
+
if (this.validateSafe(item, value[key])) this.pop(false);
|
|
333
|
+
else {
|
|
334
|
+
passed = false;
|
|
335
|
+
this.pop(true);
|
|
336
|
+
if (this.isLimitExceeded()) return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
for (const key of valueKeys)
|
|
340
|
+
/** matched patterns for unknown keys */ if (!typeKeys.has(key)) {
|
|
341
|
+
const matched = [];
|
|
342
|
+
for (const { pattern, def: propDef } of def.type.propsPatterns) if (pattern.test(key)) matched.push({
|
|
343
|
+
pattern,
|
|
344
|
+
def: propDef
|
|
345
|
+
});
|
|
346
|
+
if (matched.length) {
|
|
347
|
+
let keyPassed = false;
|
|
348
|
+
for (const { def: def$1 } of matched) if (this.validateSafe(def$1, value[key])) {
|
|
349
|
+
this.pop(false);
|
|
350
|
+
keyPassed = true;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
if (!keyPassed) {
|
|
354
|
+
this.push(key);
|
|
355
|
+
this.validateSafe(matched[0].def, value[key]);
|
|
356
|
+
this.pop(true);
|
|
357
|
+
passed = false;
|
|
358
|
+
if (this.isLimitExceeded()) return false;
|
|
359
|
+
}
|
|
360
|
+
} else if (this.opts.unknwonProps !== "ignore") {
|
|
361
|
+
if (this.opts.unknwonProps === "error") {
|
|
362
|
+
this.push(key);
|
|
363
|
+
this.error(`Unexpected property`);
|
|
364
|
+
this.pop(true);
|
|
365
|
+
if (this.isLimitExceeded()) return false;
|
|
366
|
+
passed = false;
|
|
367
|
+
} else if (this.opts.unknwonProps === "strip") delete value[key];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return passed;
|
|
371
|
+
}
|
|
372
|
+
validatePrimitive(def, value) {
|
|
373
|
+
if (typeof def.type.value !== "undefined") {
|
|
374
|
+
if (value !== def.type.value) {
|
|
375
|
+
this.error(`Expected ${def.type.value}, got ${value}`);
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
const typeOfValue = Array.isArray(value) ? "array" : typeof value;
|
|
381
|
+
switch (def.type.designType) {
|
|
382
|
+
case "never":
|
|
383
|
+
this.error(`This type is impossible, must be an internal problem`);
|
|
384
|
+
return false;
|
|
385
|
+
case "any": return true;
|
|
386
|
+
case "string":
|
|
387
|
+
if (typeOfValue !== def.type.designType) {
|
|
388
|
+
this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
return this.validateString(def, value);
|
|
392
|
+
case "number":
|
|
393
|
+
if (typeOfValue !== def.type.designType) {
|
|
394
|
+
this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
return this.validateNumber(def, value);
|
|
398
|
+
case "boolean":
|
|
399
|
+
if (typeOfValue !== def.type.designType) {
|
|
400
|
+
this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return true;
|
|
404
|
+
case "undefined":
|
|
405
|
+
if (value !== undefined) {
|
|
406
|
+
this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
return true;
|
|
410
|
+
case "null":
|
|
411
|
+
if (value !== null) {
|
|
412
|
+
this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
default: throw new Error(`Unknown type "${def.type.designType}"`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
validateString(def, value) {
|
|
420
|
+
const minLength = def.metadata.get("expect.minLength");
|
|
421
|
+
if (minLength) {
|
|
422
|
+
const length = typeof minLength === "number" ? minLength : minLength.length;
|
|
423
|
+
if (value.length < length) {
|
|
424
|
+
const message = typeof minLength === "object" && minLength.message ? minLength.message : `Expected minimum length of ${length} characters, got ${value.length} characters`;
|
|
425
|
+
this.error(message);
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const maxLength = def.metadata.get("expect.maxLength");
|
|
430
|
+
if (maxLength) {
|
|
431
|
+
const length = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
432
|
+
if (value.length > length) {
|
|
433
|
+
const message = typeof maxLength === "object" && maxLength.message ? maxLength.message : `Expected maximum length of ${length} characters, got ${value.length} characters`;
|
|
434
|
+
this.error(message);
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const patterns = def.metadata.get("expect.pattern");
|
|
439
|
+
for (const { pattern, flags, message } of patterns || []) {
|
|
440
|
+
if (!pattern) continue;
|
|
441
|
+
const cacheKey = `${pattern}//${flags || ""}`;
|
|
442
|
+
let regex = regexCache.get(cacheKey);
|
|
443
|
+
if (!regex) {
|
|
444
|
+
regex = new RegExp(pattern, flags);
|
|
445
|
+
regexCache.set(cacheKey, regex);
|
|
446
|
+
}
|
|
447
|
+
if (!regex.test(value)) {
|
|
448
|
+
this.error(message || `Value is expected to match pattern "${pattern}"`);
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
validateNumber(def, value) {
|
|
455
|
+
const int = def.metadata.get("expect.int");
|
|
456
|
+
if (int && value % 1 !== 0) {
|
|
457
|
+
const message = typeof int === "object" && int.message ? int.message : `Expected integer, got ${value}`;
|
|
458
|
+
this.error(message);
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const min = def.metadata.get("expect.min");
|
|
462
|
+
if (min) {
|
|
463
|
+
const minValue = typeof min === "number" ? min : min.minValue;
|
|
464
|
+
if (value < minValue) {
|
|
465
|
+
const message = typeof min === "object" && min.message ? min.message : `Expected minimum ${minValue}, got ${value}`;
|
|
466
|
+
this.error(message);
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const max = def.metadata.get("expect.max");
|
|
471
|
+
if (max) {
|
|
472
|
+
const maxValue = typeof max === "number" ? max : max.maxValue;
|
|
473
|
+
if (value > maxValue) {
|
|
474
|
+
const message = typeof max === "object" && max.message ? max.message : `Expected maximum ${maxValue}, got ${value}`;
|
|
475
|
+
this.error(message);
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
constructor(def, opts) {
|
|
482
|
+
_define_property(this, "def", void 0);
|
|
483
|
+
_define_property(this, "opts", void 0);
|
|
484
|
+
/** Validation errors collected during the last {@link validate} call. */ _define_property(this, "errors", void 0);
|
|
485
|
+
_define_property(this, "stackErrors", void 0);
|
|
486
|
+
_define_property(this, "stackPath", void 0);
|
|
487
|
+
this.def = def;
|
|
488
|
+
this.errors = [];
|
|
489
|
+
this.stackErrors = [];
|
|
490
|
+
this.stackPath = [];
|
|
491
|
+
this.opts = {
|
|
492
|
+
partial: false,
|
|
493
|
+
unknwonProps: "error",
|
|
494
|
+
errorLimit: 10,
|
|
495
|
+
...opts,
|
|
496
|
+
plugins: opts?.plugins || []
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
var ValidatorError = class extends Error {
|
|
501
|
+
constructor(errors) {
|
|
502
|
+
super(`${errors[0].path ? errors[0].path + ": " : ""}${errors[0].message}`), _define_property(this, "errors", void 0), _define_property(this, "name", void 0), this.errors = errors, this.name = "Validation Error";
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region packages/typescript/src/annotated-type.ts
|
|
508
|
+
function isAnnotatedType(type) {
|
|
509
|
+
return type && type.__is_atscript_annotated_type;
|
|
510
|
+
}
|
|
511
|
+
function isPhantomType(def) {
|
|
512
|
+
return def.type.kind === "" && def.type.designType === "phantom";
|
|
513
|
+
}
|
|
72
514
|
|
|
73
515
|
/** Known foorm primitive extension tags that map directly to field types. */
|
|
74
516
|
const FOORM_TAGS = new Set(['action', 'paragraph', 'select', 'radio', 'checkbox']);
|
|
@@ -85,22 +527,96 @@ function parseStaticOptions(raw) {
|
|
|
85
527
|
return String(item);
|
|
86
528
|
});
|
|
87
529
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*/
|
|
93
|
-
function resolveComputed(staticKey, fnKey, metadata, compileFn, defaultValue) {
|
|
530
|
+
// Implementation
|
|
531
|
+
function resolveProperty(fnKey, staticKey, metadata, options) {
|
|
532
|
+
const { transform, defaultValue, staticAsBoolean = false, compiler = compileFieldFn, } = options !== null && options !== void 0 ? options : {};
|
|
533
|
+
// Check for computed annotation first
|
|
94
534
|
const fnStr = metadata.get(fnKey);
|
|
95
535
|
if (typeof fnStr === 'string') {
|
|
96
|
-
return
|
|
536
|
+
return compiler(fnStr);
|
|
97
537
|
}
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
538
|
+
// Check for static annotation
|
|
539
|
+
if (staticKey !== undefined) {
|
|
540
|
+
const staticVal = metadata.get(staticKey);
|
|
541
|
+
if (staticVal !== undefined) {
|
|
542
|
+
if (staticAsBoolean) {
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
if (transform) {
|
|
546
|
+
return transform(staticVal);
|
|
547
|
+
}
|
|
548
|
+
return staticVal;
|
|
549
|
+
}
|
|
101
550
|
}
|
|
551
|
+
// Return default or undefined
|
|
102
552
|
return defaultValue;
|
|
103
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Batch resolves multiple boolean constraints at once to reduce overhead.
|
|
556
|
+
* Returns an object with all constraint values (functions or booleans).
|
|
557
|
+
*/
|
|
558
|
+
function resolveConstraints(metadata, propOptional) {
|
|
559
|
+
var _a, _b, _c, _d;
|
|
560
|
+
return {
|
|
561
|
+
optional: (_a = resolveProperty('foorm.fn.optional', undefined, metadata)) !== null && _a !== void 0 ? _a : (propOptional !== null && propOptional !== void 0 ? propOptional : false),
|
|
562
|
+
disabled: (_b = resolveProperty('foorm.fn.disabled', 'foorm.disabled', metadata, {
|
|
563
|
+
staticAsBoolean: true,
|
|
564
|
+
})) !== null && _b !== void 0 ? _b : false,
|
|
565
|
+
hidden: (_c = resolveProperty('foorm.fn.hidden', 'foorm.hidden', metadata, {
|
|
566
|
+
staticAsBoolean: true,
|
|
567
|
+
})) !== null && _c !== void 0 ? _c : false,
|
|
568
|
+
readonly: (_d = resolveProperty('foorm.fn.readonly', 'foorm.readonly', metadata, {
|
|
569
|
+
staticAsBoolean: true,
|
|
570
|
+
})) !== null && _d !== void 0 ? _d : false,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Batch resolves multiple text properties at once.
|
|
575
|
+
* Empty strings are replaced with undefined to enable Vue optimization.
|
|
576
|
+
*/
|
|
577
|
+
function resolveTextProperties(metadata, fieldName) {
|
|
578
|
+
return {
|
|
579
|
+
label: resolveProperty('foorm.fn.label', 'meta.label', metadata, {
|
|
580
|
+
defaultValue: fieldName,
|
|
581
|
+
}),
|
|
582
|
+
description: resolveProperty('foorm.fn.description', 'meta.description', metadata),
|
|
583
|
+
hint: resolveProperty('foorm.fn.hint', 'meta.hint', metadata),
|
|
584
|
+
placeholder: resolveProperty('foorm.fn.placeholder', 'meta.placeholder', metadata),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Parses @foorm.attr and @foorm.fn.attr annotations into a Record<string, TComputed<unknown>>.
|
|
589
|
+
* Static attrs are direct key-value pairs, computed attrs are compiled functions.
|
|
590
|
+
*/
|
|
591
|
+
function parseAttrs(metadata) {
|
|
592
|
+
const staticAttrs = metadata.get('foorm.attr');
|
|
593
|
+
const fnAttrs = metadata.get('foorm.fn.attr');
|
|
594
|
+
if (!staticAttrs && !fnAttrs) {
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
const result = {};
|
|
598
|
+
// Process static @foorm.attr annotations
|
|
599
|
+
if (staticAttrs) {
|
|
600
|
+
const items = Array.isArray(staticAttrs) ? staticAttrs : [staticAttrs];
|
|
601
|
+
for (const item of items) {
|
|
602
|
+
if (typeof item === 'object' && item !== null && 'name' in item && 'value' in item) {
|
|
603
|
+
const { name, value } = item;
|
|
604
|
+
result[name] = value;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Process computed @foorm.fn.attr annotations (override static if same name)
|
|
609
|
+
if (fnAttrs) {
|
|
610
|
+
const items = Array.isArray(fnAttrs) ? fnAttrs : [fnAttrs];
|
|
611
|
+
for (const item of items) {
|
|
612
|
+
if (typeof item === 'object' && item !== null && 'name' in item && 'fn' in item) {
|
|
613
|
+
const { name, fn } = item;
|
|
614
|
+
result[name] = compileFieldFn(fn);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
619
|
+
}
|
|
104
620
|
/**
|
|
105
621
|
* Converts an ATScript annotated type into a TFoormModel.
|
|
106
622
|
*
|
|
@@ -122,15 +638,18 @@ function createFoorm(type) {
|
|
|
122
638
|
const metadata = type.metadata;
|
|
123
639
|
const props = type.type.props;
|
|
124
640
|
// Form-level metadata
|
|
125
|
-
const title =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
641
|
+
const title = resolveProperty('foorm.fn.title', 'foorm.title', metadata, {
|
|
642
|
+
compiler: compileTopFn,
|
|
643
|
+
defaultValue: '',
|
|
644
|
+
});
|
|
645
|
+
const submitText = resolveProperty('foorm.fn.submit.text', 'foorm.submit.text', metadata, {
|
|
646
|
+
compiler: compileTopFn,
|
|
647
|
+
defaultValue: 'Submit',
|
|
648
|
+
});
|
|
649
|
+
const submitDisabled = resolveProperty('foorm.fn.submit.disabled', 'foorm.submit.disabled', metadata, {
|
|
650
|
+
compiler: compileTopFn,
|
|
651
|
+
defaultValue: false,
|
|
652
|
+
});
|
|
134
653
|
const submit = { text: submitText, disabled: submitDisabled };
|
|
135
654
|
// Build fields from props
|
|
136
655
|
const fields = [];
|
|
@@ -152,73 +671,42 @@ function createFoorm(type) {
|
|
|
152
671
|
}
|
|
153
672
|
}
|
|
154
673
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
674
|
+
// Add ATScript built-in validation (for @expect.* and semantic primitives like string.email)
|
|
675
|
+
// Check if field has ATScript validation requirements
|
|
676
|
+
const hasExpectConstraints = pm.get('expect.pattern') !== undefined ||
|
|
677
|
+
pm.get('expect.min') !== undefined ||
|
|
678
|
+
pm.get('expect.max') !== undefined ||
|
|
679
|
+
pm.get('expect.minLength') !== undefined ||
|
|
680
|
+
pm.get('expect.maxLength') !== undefined ||
|
|
681
|
+
pm.get('expect.int') !== undefined;
|
|
682
|
+
const hasTags = tags && tags.size > 0;
|
|
683
|
+
if (hasExpectConstraints || hasTags) {
|
|
684
|
+
// Create ATScript validator for this property
|
|
685
|
+
const propValidator = new Validator(prop);
|
|
686
|
+
validators.push((scope) => {
|
|
168
687
|
var _a;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
})(),
|
|
175
|
-
disabled: (() => {
|
|
176
|
-
const fnStr = pm.get('foorm.fn.disabled');
|
|
177
|
-
if (typeof fnStr === 'string') {
|
|
178
|
-
return compileFieldFn(fnStr);
|
|
179
|
-
}
|
|
180
|
-
return pm.get('foorm.disabled') !== undefined;
|
|
181
|
-
})(),
|
|
182
|
-
hidden: (() => {
|
|
183
|
-
const fnStr = pm.get('foorm.fn.hidden');
|
|
184
|
-
if (typeof fnStr === 'string') {
|
|
185
|
-
return compileFieldFn(fnStr);
|
|
186
|
-
}
|
|
187
|
-
return pm.get('foorm.hidden') !== undefined;
|
|
188
|
-
})(),
|
|
189
|
-
classes: (() => {
|
|
190
|
-
const fnStr = pm.get('foorm.fn.classes');
|
|
191
|
-
if (typeof fnStr === 'string') {
|
|
192
|
-
return compileFieldFn(fnStr);
|
|
688
|
+
// ATScript validator in safe mode: returns true (pass) or false (fail)
|
|
689
|
+
// On failure, propValidator.errors contains error details
|
|
690
|
+
const isValid = propValidator.validate(scope.v, true);
|
|
691
|
+
if (isValid) {
|
|
692
|
+
return true;
|
|
193
693
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
options: (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const staticOpts = pm.get('foorm.options');
|
|
209
|
-
if (staticOpts) {
|
|
210
|
-
return parseStaticOptions(staticOpts);
|
|
211
|
-
}
|
|
212
|
-
return undefined;
|
|
213
|
-
})(),
|
|
214
|
-
value: pm.get('foorm.value'),
|
|
215
|
-
validators,
|
|
694
|
+
// Extract first error message from validator.errors
|
|
695
|
+
const firstError = (_a = propValidator.errors) === null || _a === void 0 ? void 0 : _a[0];
|
|
696
|
+
return (firstError === null || firstError === void 0 ? void 0 : firstError.message) || 'Validation failed';
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
const field = Object.assign(Object.assign(Object.assign({ field: name, type: fieldType, component: pm.get('foorm.component'), autocomplete: pm.get('foorm.autocomplete'), altAction: pm.get('foorm.altAction'), order: pm.get('foorm.order'), name: name }, resolveTextProperties(pm, name)), resolveConstraints(pm, prop.optional)), {
|
|
700
|
+
// Appearance - truly optional
|
|
701
|
+
classes: resolveProperty('foorm.fn.classes', undefined, pm), styles: resolveProperty('foorm.fn.styles', undefined, pm),
|
|
702
|
+
// Data properties - truly optional
|
|
703
|
+
options: resolveProperty('foorm.fn.options', 'foorm.options', pm, {
|
|
704
|
+
transform: parseStaticOptions,
|
|
705
|
+
}), value: resolveProperty('foorm.fn.value', 'foorm.value', pm), validators,
|
|
706
|
+
// Custom attributes/props
|
|
707
|
+
attrs: parseAttrs(pm),
|
|
216
708
|
// ATScript @expect constraints
|
|
217
|
-
maxLength: pm.get('expect.maxLength'),
|
|
218
|
-
minLength: pm.get('expect.minLength'),
|
|
219
|
-
min: pm.get('expect.min'),
|
|
220
|
-
max: pm.get('expect.max'),
|
|
221
|
-
};
|
|
709
|
+
maxLength: pm.get('expect.maxLength'), minLength: pm.get('expect.minLength'), min: pm.get('expect.min'), max: pm.get('expect.max') });
|
|
222
710
|
fields.push(field);
|
|
223
711
|
}
|
|
224
712
|
// Sort by explicit order, preserving original order for unordered fields
|