@whatsaaaa/form-builder-schema 0.4.1 → 0.6.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 +109 -4
- package/dist/index.d.ts +125 -9
- package/dist/index.js +596 -153
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
+
// src/errors/definition-error.ts
|
|
3
|
+
function formatPath(path) {
|
|
4
|
+
return path.reduce((out, key) => {
|
|
5
|
+
if (typeof key === "number") return `${out}[${key}]`;
|
|
6
|
+
return out ? `${out}.${key}` : key;
|
|
7
|
+
}, "");
|
|
8
|
+
}
|
|
9
|
+
/** Thrown by `build()` when the definition has issues. Lists all of them. */
|
|
10
|
+
var DefinitionError = class extends Error {
|
|
11
|
+
issues;
|
|
12
|
+
constructor(issues) {
|
|
13
|
+
const lines = issues.map((issue) => ` ${formatPath(issue.path) || "(root)"}: ${issue.message}`);
|
|
14
|
+
super(`Invalid form definition:\n${lines.join("\n")}`);
|
|
15
|
+
this.name = "DefinitionError";
|
|
16
|
+
this.issues = issues;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
2
19
|
// src/shared/each-item.ts
|
|
3
20
|
/**
|
|
4
21
|
* Checks every item of an array with `schema` and reports the issues on the array itself.
|
|
@@ -23,9 +40,10 @@ function eachItem(schema) {
|
|
|
23
40
|
/**
|
|
24
41
|
* The schema runs in this order: empty check, base schema, normalize rules, required check,
|
|
25
42
|
* then the type's default rules and the field's rules, and last the type's `finalize()`.
|
|
43
|
+
* A container has no empty check: its value is never empty.
|
|
26
44
|
*/
|
|
27
|
-
function buildFieldSchema(checked, messages) {
|
|
28
|
-
const { field, path, type, kind } = checked;
|
|
45
|
+
function buildFieldSchema(checked, messages, ctx) {
|
|
46
|
+
const { field, path, type, kind, valueKind } = checked;
|
|
29
47
|
const required = !!field.required;
|
|
30
48
|
const requiredMessage = messages.resolve(typeof field.required === "object" && field.required.message || messages.required, {
|
|
31
49
|
field,
|
|
@@ -54,15 +72,52 @@ function buildFieldSchema(checked, messages) {
|
|
|
54
72
|
if (each.validate.length) whole.validate.unshift(eachItem(pipe(v.unknown(), each.validate)));
|
|
55
73
|
const finalize = type.finalize?.(field);
|
|
56
74
|
if (finalize) whole.validate.push(finalize);
|
|
57
|
-
const base = type.schema(field,
|
|
75
|
+
const base = type.schema(field, ctx);
|
|
58
76
|
const requiredCheck = !required ? void 0 : type.required ? type.required(requiredMessage) : kind.required?.(requiredMessage);
|
|
59
77
|
const head = pipe(base, requiredCheck ? [...whole.normalize, requiredCheck] : whole.normalize);
|
|
78
|
+
if (type.container) {
|
|
79
|
+
const rules = whole.validate.length ? [pipe(v.unknown(), whole.validate)] : [];
|
|
80
|
+
return v.optional(pipe(head, rules), valueKind === "object" ? () => ({}) : () => []);
|
|
81
|
+
}
|
|
60
82
|
const empty = required ? v.never(requiredMessage) : v.pipe(v.unknown(), v.transform(() => void 0));
|
|
61
83
|
const whenFilled = (schema) => v.lazy((input) => kind.isEmpty(input) ? empty : schema);
|
|
62
84
|
const schema = whenFilled(v.pipe(head, whenFilled(pipe(v.unknown(), whole.validate))));
|
|
63
85
|
return required ? schema : v.optional(schema);
|
|
64
86
|
}
|
|
65
87
|
const pipe = (schema, items) => items.reduce((acc, item) => v.pipe(acc, item), schema);
|
|
88
|
+
// src/shared/reserved-keys.ts
|
|
89
|
+
/**
|
|
90
|
+
* Field keys this package owns. A field type's props can't use them.
|
|
91
|
+
*/
|
|
92
|
+
const RESERVED_FIELD_KEYS = [
|
|
93
|
+
"name",
|
|
94
|
+
"type",
|
|
95
|
+
"label",
|
|
96
|
+
"description",
|
|
97
|
+
"placeholder",
|
|
98
|
+
"default",
|
|
99
|
+
"required",
|
|
100
|
+
"rules",
|
|
101
|
+
"ui"
|
|
102
|
+
];
|
|
103
|
+
/**
|
|
104
|
+
* Rule keys this package owns. A rule's params can't use them.
|
|
105
|
+
*/
|
|
106
|
+
const RESERVED_RULE_KEYS = ["rule", "message"];
|
|
107
|
+
/**
|
|
108
|
+
* Form rule keys this package owns. A form rule's params can't use them.
|
|
109
|
+
*/
|
|
110
|
+
const RESERVED_FORM_RULE_KEYS = [
|
|
111
|
+
"rule",
|
|
112
|
+
"field",
|
|
113
|
+
"message"
|
|
114
|
+
];
|
|
115
|
+
const fieldKeys = new Set(RESERVED_FIELD_KEYS);
|
|
116
|
+
const ruleKeys = new Set(RESERVED_RULE_KEYS);
|
|
117
|
+
const formRuleKeys = new Set(RESERVED_FORM_RULE_KEYS);
|
|
118
|
+
const isReservedFieldKey = (key) => fieldKeys.has(key);
|
|
119
|
+
const isReservedRuleKey = (key) => ruleKeys.has(key);
|
|
120
|
+
const isReservedFormRuleKey = (key) => formRuleKeys.has(key);
|
|
66
121
|
// src/field-types/basic-types.ts
|
|
67
122
|
/** Types with one plain value and no props. Each maps to a native input. */
|
|
68
123
|
const basicTypes = {
|
|
@@ -147,6 +202,26 @@ const choiceTypes = {
|
|
|
147
202
|
schema: (field) => v.array(oneOf(field.options))
|
|
148
203
|
})
|
|
149
204
|
};
|
|
205
|
+
// src/field-types/container-types.ts
|
|
206
|
+
const childField = v.custom(() => true);
|
|
207
|
+
/** Types whose value holds other fields. Display elements can go inside them too. */
|
|
208
|
+
const containerTypes = {
|
|
209
|
+
group: defineFieldType({
|
|
210
|
+
kind: "object",
|
|
211
|
+
container: true,
|
|
212
|
+
props: v.object({ fields: v.array(childField) }),
|
|
213
|
+
schema: (field, ctx) => ctx.buildFields(field.fields, [...ctx.path, "fields"])
|
|
214
|
+
}),
|
|
215
|
+
array: defineFieldType({
|
|
216
|
+
kind: "array",
|
|
217
|
+
container: true,
|
|
218
|
+
props: v.pipe(v.object({
|
|
219
|
+
of: v.optional(childField),
|
|
220
|
+
fields: v.optional(v.array(childField))
|
|
221
|
+
}), v.check((props) => props.of === void 0 !== (props.fields === void 0), "Give exactly one of \"of\" and \"fields\"")),
|
|
222
|
+
schema: (field, ctx) => v.array(field.of ? ctx.buildField(field.of, [...ctx.path, "of"]) : ctx.buildFields(field.fields ?? [], [...ctx.path, "fields"]))
|
|
223
|
+
})
|
|
224
|
+
};
|
|
150
225
|
// src/field-types/date-types.ts
|
|
151
226
|
const outputProps = v.object({
|
|
152
227
|
/** `'date'` turns the ISO string into a `Date`. Defaults to `'string'`. */
|
|
@@ -221,7 +296,8 @@ const defaultFieldTypes = {
|
|
|
221
296
|
...dateTypes,
|
|
222
297
|
...choiceTypes,
|
|
223
298
|
file: fileType,
|
|
224
|
-
...displayTypes
|
|
299
|
+
...displayTypes,
|
|
300
|
+
...containerTypes
|
|
225
301
|
};
|
|
226
302
|
// src/kinds/default-kinds.ts
|
|
227
303
|
const isNil = (input) => input === void 0 || input === null;
|
|
@@ -272,7 +348,7 @@ const any = [
|
|
|
272
348
|
"string",
|
|
273
349
|
"boolean"
|
|
274
350
|
];
|
|
275
|
-
const meta$
|
|
351
|
+
const meta$7 = (label) => ({
|
|
276
352
|
label,
|
|
277
353
|
category: "comparison"
|
|
278
354
|
});
|
|
@@ -283,54 +359,54 @@ const comparisonRules = {
|
|
|
283
359
|
minValue: defineRule({
|
|
284
360
|
kinds: ordered,
|
|
285
361
|
params: valueParams,
|
|
286
|
-
meta: meta$
|
|
362
|
+
meta: meta$7("Minimum"),
|
|
287
363
|
create: (p, m) => v.minValue(p.value, m)
|
|
288
364
|
}),
|
|
289
365
|
maxValue: defineRule({
|
|
290
366
|
kinds: ordered,
|
|
291
367
|
params: valueParams,
|
|
292
|
-
meta: meta$
|
|
368
|
+
meta: meta$7("Maximum"),
|
|
293
369
|
create: (p, m) => v.maxValue(p.value, m)
|
|
294
370
|
}),
|
|
295
371
|
gtValue: defineRule({
|
|
296
372
|
kinds: ordered,
|
|
297
373
|
params: valueParams,
|
|
298
|
-
meta: meta$
|
|
374
|
+
meta: meta$7("Greater than"),
|
|
299
375
|
create: (p, m) => v.gtValue(p.value, m)
|
|
300
376
|
}),
|
|
301
377
|
ltValue: defineRule({
|
|
302
378
|
kinds: ordered,
|
|
303
379
|
params: valueParams,
|
|
304
|
-
meta: meta$
|
|
380
|
+
meta: meta$7("Less than"),
|
|
305
381
|
create: (p, m) => v.ltValue(p.value, m)
|
|
306
382
|
}),
|
|
307
383
|
value: defineRule({
|
|
308
384
|
kinds: any,
|
|
309
385
|
params: valueParams,
|
|
310
|
-
meta: meta$
|
|
386
|
+
meta: meta$7("Equals"),
|
|
311
387
|
create: (p, m) => v.value(p.value, m)
|
|
312
388
|
}),
|
|
313
389
|
notValue: defineRule({
|
|
314
390
|
kinds: any,
|
|
315
391
|
params: valueParams,
|
|
316
|
-
meta: meta$
|
|
392
|
+
meta: meta$7("Does not equal"),
|
|
317
393
|
create: (p, m) => v.notValue(p.value, m)
|
|
318
394
|
}),
|
|
319
395
|
values: defineRule({
|
|
320
396
|
kinds: ordered,
|
|
321
397
|
params: valuesParams,
|
|
322
|
-
meta: meta$
|
|
398
|
+
meta: meta$7("One of"),
|
|
323
399
|
create: (p, m) => v.values(p.values, m)
|
|
324
400
|
}),
|
|
325
401
|
notValues: defineRule({
|
|
326
402
|
kinds: ordered,
|
|
327
403
|
params: valuesParams,
|
|
328
|
-
meta: meta$
|
|
404
|
+
meta: meta$7("None of"),
|
|
329
405
|
create: (p, m) => v.notValues(p.values, m)
|
|
330
406
|
})
|
|
331
407
|
};
|
|
332
408
|
// src/rules/content-rules.ts
|
|
333
|
-
const meta$
|
|
409
|
+
const meta$6 = (label, description) => ({
|
|
334
410
|
label,
|
|
335
411
|
description,
|
|
336
412
|
category: "content"
|
|
@@ -359,31 +435,31 @@ const contentRules = {
|
|
|
359
435
|
regex: defineRule({
|
|
360
436
|
kinds: ["string"],
|
|
361
437
|
params: regexParams,
|
|
362
|
-
meta: meta$
|
|
438
|
+
meta: meta$6("Matches pattern", "A JavaScript regular expression, without the slashes."),
|
|
363
439
|
create: (p, m) => v.regex(new RegExp(p.pattern, p.flags?.replace(/[gy]/g, "")), m)
|
|
364
440
|
}),
|
|
365
441
|
startsWith: defineRule({
|
|
366
442
|
kinds: ["string"],
|
|
367
443
|
params: stringParams,
|
|
368
|
-
meta: meta$
|
|
444
|
+
meta: meta$6("Starts with"),
|
|
369
445
|
create: (p, m) => v.startsWith(p.value, m)
|
|
370
446
|
}),
|
|
371
447
|
endsWith: defineRule({
|
|
372
448
|
kinds: ["string"],
|
|
373
449
|
params: stringParams,
|
|
374
|
-
meta: meta$
|
|
450
|
+
meta: meta$6("Ends with"),
|
|
375
451
|
create: (p, m) => v.endsWith(p.value, m)
|
|
376
452
|
}),
|
|
377
453
|
includes: defineRule({
|
|
378
454
|
kinds: ["string", "array"],
|
|
379
455
|
params: contentParams,
|
|
380
|
-
meta: meta$
|
|
456
|
+
meta: meta$6("Contains"),
|
|
381
457
|
create: (p, m) => v.includes(p.value, m)
|
|
382
458
|
}),
|
|
383
459
|
excludes: defineRule({
|
|
384
460
|
kinds: ["string", "array"],
|
|
385
461
|
params: contentParams,
|
|
386
|
-
meta: meta$
|
|
462
|
+
meta: meta$6("Does not contain"),
|
|
387
463
|
create: (p, m) => v.excludes(p.value, m)
|
|
388
464
|
})
|
|
389
465
|
};
|
|
@@ -407,7 +483,7 @@ const dateRules = {
|
|
|
407
483
|
};
|
|
408
484
|
// src/rules/file-rules.ts
|
|
409
485
|
const kinds$3 = ["file"];
|
|
410
|
-
const meta$
|
|
486
|
+
const meta$5 = (label, description) => ({
|
|
411
487
|
label,
|
|
412
488
|
description,
|
|
413
489
|
category: "file"
|
|
@@ -419,31 +495,31 @@ const fileRules = {
|
|
|
419
495
|
minSize: defineRule({
|
|
420
496
|
kinds: kinds$3,
|
|
421
497
|
params: countParams,
|
|
422
|
-
meta: meta$
|
|
498
|
+
meta: meta$5("Minimum size", bytes),
|
|
423
499
|
create: (p, m) => v.minSize(p.value, m)
|
|
424
500
|
}),
|
|
425
501
|
maxSize: defineRule({
|
|
426
502
|
kinds: kinds$3,
|
|
427
503
|
params: countParams,
|
|
428
|
-
meta: meta$
|
|
504
|
+
meta: meta$5("Maximum size", bytes),
|
|
429
505
|
create: (p, m) => v.maxSize(p.value, m)
|
|
430
506
|
}),
|
|
431
507
|
size: defineRule({
|
|
432
508
|
kinds: kinds$3,
|
|
433
509
|
params: countParams,
|
|
434
|
-
meta: meta$
|
|
510
|
+
meta: meta$5("Exact size", bytes),
|
|
435
511
|
create: (p, m) => v.size(p.value, m)
|
|
436
512
|
}),
|
|
437
513
|
notSize: defineRule({
|
|
438
514
|
kinds: kinds$3,
|
|
439
515
|
params: countParams,
|
|
440
|
-
meta: meta$
|
|
516
|
+
meta: meta$5("Not this size", bytes),
|
|
441
517
|
create: (p, m) => v.notSize(p.value, m)
|
|
442
518
|
}),
|
|
443
519
|
mimeType: defineRule({
|
|
444
520
|
kinds: kinds$3,
|
|
445
521
|
params: mimeTypeParams,
|
|
446
|
-
meta: meta$
|
|
522
|
+
meta: meta$5("File type"),
|
|
447
523
|
create: (p, m) => v.mimeType(p.types, m)
|
|
448
524
|
})
|
|
449
525
|
};
|
|
@@ -514,7 +590,7 @@ const formatRules = {
|
|
|
514
590
|
};
|
|
515
591
|
// src/rules/length-rules.ts
|
|
516
592
|
const kinds$2 = ["string", "array"];
|
|
517
|
-
const meta$
|
|
593
|
+
const meta$4 = (label) => ({
|
|
518
594
|
label,
|
|
519
595
|
category: "length"
|
|
520
596
|
});
|
|
@@ -523,31 +599,31 @@ const lengthRules = {
|
|
|
523
599
|
length: defineRule({
|
|
524
600
|
kinds: kinds$2,
|
|
525
601
|
params: countParams,
|
|
526
|
-
meta: meta$
|
|
602
|
+
meta: meta$4("Exact length"),
|
|
527
603
|
create: (p, m) => v.length(p.value, m)
|
|
528
604
|
}),
|
|
529
605
|
minLength: defineRule({
|
|
530
606
|
kinds: kinds$2,
|
|
531
607
|
params: countParams,
|
|
532
|
-
meta: meta$
|
|
608
|
+
meta: meta$4("Minimum length"),
|
|
533
609
|
create: (p, m) => v.minLength(p.value, m)
|
|
534
610
|
}),
|
|
535
611
|
maxLength: defineRule({
|
|
536
612
|
kinds: kinds$2,
|
|
537
613
|
params: countParams,
|
|
538
|
-
meta: meta$
|
|
614
|
+
meta: meta$4("Maximum length"),
|
|
539
615
|
create: (p, m) => v.maxLength(p.value, m)
|
|
540
616
|
}),
|
|
541
617
|
notLength: defineRule({
|
|
542
618
|
kinds: kinds$2,
|
|
543
619
|
params: countParams,
|
|
544
|
-
meta: meta$
|
|
620
|
+
meta: meta$4("Not this length"),
|
|
545
621
|
create: (p, m) => v.notLength(p.value, m)
|
|
546
622
|
})
|
|
547
623
|
};
|
|
548
624
|
// src/rules/normalize-rules.ts
|
|
549
625
|
const stage = "normalize";
|
|
550
|
-
const meta$
|
|
626
|
+
const meta$3 = (label, description) => ({
|
|
551
627
|
label,
|
|
552
628
|
description,
|
|
553
629
|
category: "normalize"
|
|
@@ -561,31 +637,31 @@ const normalizeRules = {
|
|
|
561
637
|
trim: defineRule({
|
|
562
638
|
kinds: ["string"],
|
|
563
639
|
stage,
|
|
564
|
-
meta: meta$
|
|
640
|
+
meta: meta$3("Trim"),
|
|
565
641
|
create: () => v.trim()
|
|
566
642
|
}),
|
|
567
643
|
trimStart: defineRule({
|
|
568
644
|
kinds: ["string"],
|
|
569
645
|
stage,
|
|
570
|
-
meta: meta$
|
|
646
|
+
meta: meta$3("Trim start"),
|
|
571
647
|
create: () => v.trimStart()
|
|
572
648
|
}),
|
|
573
649
|
trimEnd: defineRule({
|
|
574
650
|
kinds: ["string"],
|
|
575
651
|
stage,
|
|
576
|
-
meta: meta$
|
|
652
|
+
meta: meta$3("Trim end"),
|
|
577
653
|
create: () => v.trimEnd()
|
|
578
654
|
}),
|
|
579
655
|
toLowerCase: defineRule({
|
|
580
656
|
kinds: ["string"],
|
|
581
657
|
stage,
|
|
582
|
-
meta: meta$
|
|
658
|
+
meta: meta$3("Lowercase"),
|
|
583
659
|
create: () => v.toLowerCase()
|
|
584
660
|
}),
|
|
585
661
|
toUpperCase: defineRule({
|
|
586
662
|
kinds: ["string"],
|
|
587
663
|
stage,
|
|
588
|
-
meta: meta$
|
|
664
|
+
meta: meta$3("Uppercase"),
|
|
589
665
|
create: () => v.toUpperCase()
|
|
590
666
|
}),
|
|
591
667
|
normalize: defineRule({
|
|
@@ -597,27 +673,27 @@ const normalizeRules = {
|
|
|
597
673
|
"NFKC",
|
|
598
674
|
"NFKD"
|
|
599
675
|
])) }),
|
|
600
|
-
meta: meta$
|
|
676
|
+
meta: meta$3("Unicode normalize", "Defaults to NFC."),
|
|
601
677
|
create: (p) => v.normalize(p.form)
|
|
602
678
|
}),
|
|
603
679
|
toMinValue: defineRule({
|
|
604
680
|
kinds: ["number"],
|
|
605
681
|
stage,
|
|
606
682
|
params: limit,
|
|
607
|
-
meta: meta$
|
|
683
|
+
meta: meta$3("Raise to minimum", "Replaces a smaller number with the minimum."),
|
|
608
684
|
create: (p) => v.toMinValue(p.value)
|
|
609
685
|
}),
|
|
610
686
|
toMaxValue: defineRule({
|
|
611
687
|
kinds: ["number"],
|
|
612
688
|
stage,
|
|
613
689
|
params: limit,
|
|
614
|
-
meta: meta$
|
|
690
|
+
meta: meta$3("Lower to maximum", "Replaces a larger number with the maximum."),
|
|
615
691
|
create: (p) => v.toMaxValue(p.value)
|
|
616
692
|
})
|
|
617
693
|
};
|
|
618
694
|
// src/rules/number-rules.ts
|
|
619
695
|
const kinds$1 = ["number"];
|
|
620
|
-
const meta$
|
|
696
|
+
const meta$2 = (label) => ({
|
|
621
697
|
label,
|
|
622
698
|
category: "number"
|
|
623
699
|
});
|
|
@@ -626,29 +702,29 @@ const divisor = v.pipe(v.number(), v.finite(), v.notValue(0, "Can't be 0"));
|
|
|
626
702
|
const numberRules = {
|
|
627
703
|
integer: defineRule({
|
|
628
704
|
kinds: kinds$1,
|
|
629
|
-
meta: meta$
|
|
705
|
+
meta: meta$2("Whole number"),
|
|
630
706
|
create: (_, m) => v.integer(m)
|
|
631
707
|
}),
|
|
632
708
|
safeInteger: defineRule({
|
|
633
709
|
kinds: kinds$1,
|
|
634
|
-
meta: meta$
|
|
710
|
+
meta: meta$2("Safe whole number"),
|
|
635
711
|
create: (_, m) => v.safeInteger(m)
|
|
636
712
|
}),
|
|
637
713
|
finite: defineRule({
|
|
638
714
|
kinds: kinds$1,
|
|
639
|
-
meta: meta$
|
|
715
|
+
meta: meta$2("Finite number"),
|
|
640
716
|
create: (_, m) => v.finite(m)
|
|
641
717
|
}),
|
|
642
718
|
multipleOf: defineRule({
|
|
643
719
|
kinds: kinds$1,
|
|
644
720
|
params: v.object({ value: divisor }),
|
|
645
|
-
meta: meta$
|
|
721
|
+
meta: meta$2("Multiple of"),
|
|
646
722
|
create: (p, m) => v.multipleOf(p.value, m)
|
|
647
723
|
})
|
|
648
724
|
};
|
|
649
725
|
// src/rules/text-unit-rules.ts
|
|
650
726
|
const kinds = ["string"];
|
|
651
|
-
const meta = (label, description) => ({
|
|
727
|
+
const meta$1 = (label, description) => ({
|
|
652
728
|
label,
|
|
653
729
|
description,
|
|
654
730
|
category: "text"
|
|
@@ -670,73 +746,73 @@ const textUnitRules = {
|
|
|
670
746
|
graphemes: defineRule({
|
|
671
747
|
kinds,
|
|
672
748
|
params: countParams,
|
|
673
|
-
meta: meta("Exact characters", graphemesInfo),
|
|
749
|
+
meta: meta$1("Exact characters", graphemesInfo),
|
|
674
750
|
create: (p, m) => v.graphemes(p.value, m)
|
|
675
751
|
}),
|
|
676
752
|
minGraphemes: defineRule({
|
|
677
753
|
kinds,
|
|
678
754
|
params: countParams,
|
|
679
|
-
meta: meta("Minimum characters", graphemesInfo),
|
|
755
|
+
meta: meta$1("Minimum characters", graphemesInfo),
|
|
680
756
|
create: (p, m) => v.minGraphemes(p.value, m)
|
|
681
757
|
}),
|
|
682
758
|
maxGraphemes: defineRule({
|
|
683
759
|
kinds,
|
|
684
760
|
params: countParams,
|
|
685
|
-
meta: meta("Maximum characters", graphemesInfo),
|
|
761
|
+
meta: meta$1("Maximum characters", graphemesInfo),
|
|
686
762
|
create: (p, m) => v.maxGraphemes(p.value, m)
|
|
687
763
|
}),
|
|
688
764
|
notGraphemes: defineRule({
|
|
689
765
|
kinds,
|
|
690
766
|
params: countParams,
|
|
691
|
-
meta: meta("Not this many characters", graphemesInfo),
|
|
767
|
+
meta: meta$1("Not this many characters", graphemesInfo),
|
|
692
768
|
create: (p, m) => v.notGraphemes(p.value, m)
|
|
693
769
|
}),
|
|
694
770
|
words: defineRule({
|
|
695
771
|
kinds,
|
|
696
772
|
params: wordsParams,
|
|
697
|
-
meta: meta("Exact words"),
|
|
773
|
+
meta: meta$1("Exact words"),
|
|
698
774
|
create: (p, m) => v.words(p.locale, p.value, m)
|
|
699
775
|
}),
|
|
700
776
|
minWords: defineRule({
|
|
701
777
|
kinds,
|
|
702
778
|
params: wordsParams,
|
|
703
|
-
meta: meta("Minimum words"),
|
|
779
|
+
meta: meta$1("Minimum words"),
|
|
704
780
|
create: (p, m) => v.minWords(p.locale, p.value, m)
|
|
705
781
|
}),
|
|
706
782
|
maxWords: defineRule({
|
|
707
783
|
kinds,
|
|
708
784
|
params: wordsParams,
|
|
709
|
-
meta: meta("Maximum words"),
|
|
785
|
+
meta: meta$1("Maximum words"),
|
|
710
786
|
create: (p, m) => v.maxWords(p.locale, p.value, m)
|
|
711
787
|
}),
|
|
712
788
|
notWords: defineRule({
|
|
713
789
|
kinds,
|
|
714
790
|
params: wordsParams,
|
|
715
|
-
meta: meta("Not this many words"),
|
|
791
|
+
meta: meta$1("Not this many words"),
|
|
716
792
|
create: (p, m) => v.notWords(p.locale, p.value, m)
|
|
717
793
|
}),
|
|
718
794
|
bytes: defineRule({
|
|
719
795
|
kinds,
|
|
720
796
|
params: countParams,
|
|
721
|
-
meta: meta("Exact bytes", bytesInfo),
|
|
797
|
+
meta: meta$1("Exact bytes", bytesInfo),
|
|
722
798
|
create: (p, m) => v.bytes(p.value, m)
|
|
723
799
|
}),
|
|
724
800
|
minBytes: defineRule({
|
|
725
801
|
kinds,
|
|
726
802
|
params: countParams,
|
|
727
|
-
meta: meta("Minimum bytes", bytesInfo),
|
|
803
|
+
meta: meta$1("Minimum bytes", bytesInfo),
|
|
728
804
|
create: (p, m) => v.minBytes(p.value, m)
|
|
729
805
|
}),
|
|
730
806
|
maxBytes: defineRule({
|
|
731
807
|
kinds,
|
|
732
808
|
params: countParams,
|
|
733
|
-
meta: meta("Maximum bytes", bytesInfo),
|
|
809
|
+
meta: meta$1("Maximum bytes", bytesInfo),
|
|
734
810
|
create: (p, m) => v.maxBytes(p.value, m)
|
|
735
811
|
}),
|
|
736
812
|
notBytes: defineRule({
|
|
737
813
|
kinds,
|
|
738
814
|
params: countParams,
|
|
739
|
-
meta: meta("Not this many bytes", bytesInfo),
|
|
815
|
+
meta: meta$1("Not this many bytes", bytesInfo),
|
|
740
816
|
create: (p, m) => v.notBytes(p.value, m)
|
|
741
817
|
})
|
|
742
818
|
};
|
|
@@ -753,10 +829,71 @@ const defaultRules = {
|
|
|
753
829
|
...fileRules,
|
|
754
830
|
...normalizeRules
|
|
755
831
|
};
|
|
832
|
+
// src/rules/define-form-rule.ts
|
|
833
|
+
const defineFormRule = (def) => def;
|
|
834
|
+
// src/rules/form-rules.ts
|
|
835
|
+
const meta = (label, description) => ({
|
|
836
|
+
label,
|
|
837
|
+
description,
|
|
838
|
+
category: "comparison"
|
|
839
|
+
});
|
|
840
|
+
const comparable = (value) => value instanceof Date ? value.getTime() : value;
|
|
841
|
+
const otherParams = v.object({ other: v.string() });
|
|
842
|
+
const operators = {
|
|
843
|
+
lt: (a, b) => a < b,
|
|
844
|
+
lte: (a, b) => a <= b,
|
|
845
|
+
gt: (a, b) => a > b,
|
|
846
|
+
gte: (a, b) => a >= b
|
|
847
|
+
};
|
|
848
|
+
/** Every built-in form rule. */
|
|
849
|
+
const defaultFormRules = {
|
|
850
|
+
sameAs: defineFormRule({
|
|
851
|
+
params: otherParams,
|
|
852
|
+
reads: (params) => [params.other],
|
|
853
|
+
kinds: [
|
|
854
|
+
"string",
|
|
855
|
+
"number",
|
|
856
|
+
"boolean",
|
|
857
|
+
"date"
|
|
858
|
+
],
|
|
859
|
+
sameKind: true,
|
|
860
|
+
meta: meta("Same as", "Equals another field, e.g. to confirm a password"),
|
|
861
|
+
check: ([value, other]) => comparable(value) === comparable(other)
|
|
862
|
+
}),
|
|
863
|
+
differentFrom: defineFormRule({
|
|
864
|
+
params: otherParams,
|
|
865
|
+
reads: (params) => [params.other],
|
|
866
|
+
kinds: [
|
|
867
|
+
"string",
|
|
868
|
+
"number",
|
|
869
|
+
"boolean",
|
|
870
|
+
"date"
|
|
871
|
+
],
|
|
872
|
+
sameKind: true,
|
|
873
|
+
meta: meta("Different from", "Differs from another field, e.g. a new password"),
|
|
874
|
+
check: ([value, other]) => comparable(value) !== comparable(other)
|
|
875
|
+
}),
|
|
876
|
+
compare: defineFormRule({
|
|
877
|
+
params: v.object({
|
|
878
|
+
op: v.picklist([
|
|
879
|
+
"lt",
|
|
880
|
+
"lte",
|
|
881
|
+
"gt",
|
|
882
|
+
"gte"
|
|
883
|
+
]),
|
|
884
|
+
other: v.string()
|
|
885
|
+
}),
|
|
886
|
+
reads: (params) => [params.other],
|
|
887
|
+
kinds: ["number", "date"],
|
|
888
|
+
sameKind: true,
|
|
889
|
+
meta: meta("Compare", "Less or greater than another field, e.g. an end date after the start"),
|
|
890
|
+
check: ([value, other], { op }) => operators[op](comparable(value), comparable(other))
|
|
891
|
+
})
|
|
892
|
+
};
|
|
756
893
|
// src/builder/registry.ts
|
|
757
894
|
/** Consumer entries replace defaults with the same key. */
|
|
758
895
|
function mergeRegistry(custom) {
|
|
759
|
-
const
|
|
896
|
+
const defaults = custom.includeDefaultRules !== false;
|
|
760
897
|
return {
|
|
761
898
|
kinds: Object.freeze({
|
|
762
899
|
...defaultKinds,
|
|
@@ -767,31 +904,18 @@ function mergeRegistry(custom) {
|
|
|
767
904
|
...custom.fieldTypes
|
|
768
905
|
}),
|
|
769
906
|
rules: Object.freeze({
|
|
770
|
-
...
|
|
907
|
+
...defaults ? defaultRules : {},
|
|
771
908
|
...custom.rules
|
|
909
|
+
}),
|
|
910
|
+
formRules: Object.freeze({
|
|
911
|
+
...defaults ? defaultFormRules : {},
|
|
912
|
+
...custom.formRules
|
|
772
913
|
})
|
|
773
914
|
};
|
|
774
915
|
}
|
|
775
916
|
function lookup(record, key) {
|
|
776
917
|
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
777
918
|
}
|
|
778
|
-
// src/errors/definition-error.ts
|
|
779
|
-
function formatPath(path) {
|
|
780
|
-
return path.reduce((out, key) => {
|
|
781
|
-
if (typeof key === "number") return `${out}[${key}]`;
|
|
782
|
-
return out ? `${out}.${key}` : key;
|
|
783
|
-
}, "");
|
|
784
|
-
}
|
|
785
|
-
/** Thrown by `build()` when the definition has issues. Lists all of them. */
|
|
786
|
-
var DefinitionError = class extends Error {
|
|
787
|
-
issues;
|
|
788
|
-
constructor(issues) {
|
|
789
|
-
const lines = issues.map((issue) => ` ${formatPath(issue.path) || "(root)"}: ${issue.message}`);
|
|
790
|
-
super(`Invalid form definition:\n${lines.join("\n")}`);
|
|
791
|
-
this.name = "DefinitionError";
|
|
792
|
-
this.issues = issues;
|
|
793
|
-
}
|
|
794
|
-
};
|
|
795
919
|
// src/rules/rule-params.ts
|
|
796
920
|
const cache = /* @__PURE__ */ new WeakMap();
|
|
797
921
|
function paramsFor(def, kind) {
|
|
@@ -808,34 +932,7 @@ function paramsFor(def, kind) {
|
|
|
808
932
|
}
|
|
809
933
|
return params;
|
|
810
934
|
}
|
|
811
|
-
// src/shared/reserved-keys.ts
|
|
812
|
-
/**
|
|
813
|
-
* Field keys this package owns. A field type's props can't use them.
|
|
814
|
-
*/
|
|
815
|
-
const RESERVED_FIELD_KEYS = [
|
|
816
|
-
"name",
|
|
817
|
-
"type",
|
|
818
|
-
"label",
|
|
819
|
-
"description",
|
|
820
|
-
"placeholder",
|
|
821
|
-
"default",
|
|
822
|
-
"required",
|
|
823
|
-
"rules",
|
|
824
|
-
"ui"
|
|
825
|
-
];
|
|
826
|
-
/**
|
|
827
|
-
* Rule keys this package owns. A rule's params can't use them.
|
|
828
|
-
*/
|
|
829
|
-
const RESERVED_RULE_KEYS = ["rule", "message"];
|
|
830
|
-
const fieldKeys = new Set(RESERVED_FIELD_KEYS);
|
|
831
|
-
const ruleKeys = new Set(RESERVED_RULE_KEYS);
|
|
832
|
-
const isReservedFieldKey = (key) => fieldKeys.has(key);
|
|
833
|
-
const isReservedRuleKey = (key) => ruleKeys.has(key);
|
|
834
935
|
// src/builder/validate-definition.ts
|
|
835
|
-
const formShape = v.object({
|
|
836
|
-
version: v.string(),
|
|
837
|
-
fields: v.array(v.unknown())
|
|
838
|
-
});
|
|
839
936
|
const fieldShape = v.looseObject({
|
|
840
937
|
name: v.optional(v.string()),
|
|
841
938
|
type: v.string(),
|
|
@@ -847,40 +944,18 @@ const fieldShape = v.looseObject({
|
|
|
847
944
|
rules: v.optional(v.array(v.unknown())),
|
|
848
945
|
ui: v.optional(v.record(v.string(), v.unknown()))
|
|
849
946
|
});
|
|
850
|
-
const ruleShape = v.looseObject({
|
|
947
|
+
const ruleShape$1 = v.looseObject({
|
|
851
948
|
rule: v.string(),
|
|
852
949
|
message: v.optional(v.string())
|
|
853
950
|
});
|
|
854
951
|
const noParams = v.object({});
|
|
855
952
|
/**
|
|
856
|
-
* Checks
|
|
953
|
+
* Checks one field's shape, name, type, props and rules, and adds its issues to `issues`.
|
|
954
|
+
* Returns the field ready to build, or undefined when it has issues or no value.
|
|
955
|
+
*
|
|
956
|
+
* `names` holds the names used so far at this level. It is undefined for the field of a list's
|
|
957
|
+
* items, which needs no name.
|
|
857
958
|
*/
|
|
858
|
-
function checkDefinition(json, registry) {
|
|
859
|
-
const issues = [];
|
|
860
|
-
const fields = [];
|
|
861
|
-
const form = v.safeParse(formShape, json);
|
|
862
|
-
if (!form.success) return {
|
|
863
|
-
issues: toIssues("invalid_shape", [], form.issues),
|
|
864
|
-
fields
|
|
865
|
-
};
|
|
866
|
-
if (form.output.version !== "1") return {
|
|
867
|
-
issues: [{
|
|
868
|
-
code: "unsupported_version",
|
|
869
|
-
path: ["version"],
|
|
870
|
-
message: `Unsupported version "${form.output.version}". This package reads version '1'.`
|
|
871
|
-
}],
|
|
872
|
-
fields
|
|
873
|
-
};
|
|
874
|
-
const names = /* @__PURE__ */ new Set();
|
|
875
|
-
form.output.fields.forEach((raw, i) => {
|
|
876
|
-
const field = checkField(raw, ["fields", i], registry, names, issues);
|
|
877
|
-
if (field) fields.push(field);
|
|
878
|
-
});
|
|
879
|
-
return {
|
|
880
|
-
issues,
|
|
881
|
-
fields
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
959
|
function checkField(raw, path, registry, names, issues) {
|
|
885
960
|
const shape = v.safeParse(fieldShape, raw);
|
|
886
961
|
if (!shape.success) {
|
|
@@ -891,9 +966,13 @@ function checkField(raw, path, registry, names, issues) {
|
|
|
891
966
|
const issueCount = issues.length;
|
|
892
967
|
const type = lookup(registry.fieldTypes, field.type);
|
|
893
968
|
const hasValue = type?.kind !== "none";
|
|
894
|
-
if (!field.name) {
|
|
969
|
+
if (!names) {} else if (!field.name) {
|
|
895
970
|
if (type && hasValue) issues.push(issue("missing_name", [...path, "name"], "A field needs a name"));
|
|
896
971
|
} else if (hasValue) {
|
|
972
|
+
if (/[.[\]]/.test(field.name)) {
|
|
973
|
+
const message = `Name "${field.name}" can't contain ".", "[" or "]"`;
|
|
974
|
+
issues.push(issue("invalid_name", [...path, "name"], message));
|
|
975
|
+
}
|
|
897
976
|
if (names.has(field.name)) issues.push(issue("duplicate_name", [...path, "name"], `Duplicate name "${field.name}"`));
|
|
898
977
|
names.add(field.name);
|
|
899
978
|
}
|
|
@@ -907,6 +986,10 @@ function checkField(raw, path, registry, names, issues) {
|
|
|
907
986
|
const message = `A "${field.type}" field has no value, so it can't have "${key}"`;
|
|
908
987
|
issues.push(issue("invalid_shape", [...path, key], message));
|
|
909
988
|
}
|
|
989
|
+
if (!names) {
|
|
990
|
+
const message = `A list's items need a value, and a "${field.type}" field has none`;
|
|
991
|
+
issues.push(issue("invalid_shape", [...path, "type"], message));
|
|
992
|
+
}
|
|
910
993
|
return;
|
|
911
994
|
}
|
|
912
995
|
const withProps = props && {
|
|
@@ -919,12 +1002,18 @@ function checkField(raw, path, registry, names, issues) {
|
|
|
919
1002
|
kind,
|
|
920
1003
|
itemKind: kind === "array" ? resolveKind(type.itemKind, withProps) : void 0
|
|
921
1004
|
};
|
|
1005
|
+
if (type.container && kind === "object") {
|
|
1006
|
+
for (const key of ["required", "default"]) if (field[key] !== void 0) {
|
|
1007
|
+
const message = `A "${field.type}" field is always there, so it can't have "${key}"`;
|
|
1008
|
+
issues.push(issue("invalid_shape", [...path, key], message));
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
922
1011
|
const fieldRules = (shape.output.rules ?? []).flatMap((rule, j) => checkRule(rule, kinds, [
|
|
923
1012
|
...path,
|
|
924
1013
|
"rules",
|
|
925
1014
|
j
|
|
926
1015
|
], registry, issues) ?? []);
|
|
927
|
-
if (issues.length > issueCount || !field.name || !withProps) return void 0;
|
|
1016
|
+
if (issues.length > issueCount || names && !field.name || !withProps) return void 0;
|
|
928
1017
|
const overridden = new Set(fieldRules.map((rule) => rule.name));
|
|
929
1018
|
const defaultRules = checkDefaultRules(field.type, type, kinds, registry).filter((rule) => !overridden.has(rule.name));
|
|
930
1019
|
return {
|
|
@@ -933,6 +1022,7 @@ function checkField(raw, path, registry, names, issues) {
|
|
|
933
1022
|
path,
|
|
934
1023
|
type,
|
|
935
1024
|
kind: kindDef(field.type, kind, registry),
|
|
1025
|
+
valueKind: kind,
|
|
936
1026
|
rules: [...defaultRules, ...fieldRules]
|
|
937
1027
|
};
|
|
938
1028
|
}
|
|
@@ -970,7 +1060,7 @@ function checkProps(field, type, path, issues) {
|
|
|
970
1060
|
issues.push(...toIssues("invalid_props", path, parsed.issues));
|
|
971
1061
|
}
|
|
972
1062
|
function checkRule(raw, { kind, itemKind }, path, registry, issues) {
|
|
973
|
-
const shape = v.safeParse(ruleShape, raw);
|
|
1063
|
+
const shape = v.safeParse(ruleShape$1, raw);
|
|
974
1064
|
if (!shape.success) {
|
|
975
1065
|
issues.push(...toIssues("invalid_shape", path, shape.issues));
|
|
976
1066
|
return;
|
|
@@ -988,31 +1078,42 @@ function checkRule(raw, { kind, itemKind }, path, registry, issues) {
|
|
|
988
1078
|
issues.push(issue("kind_mismatch", [...path, "rule"], message));
|
|
989
1079
|
return;
|
|
990
1080
|
}
|
|
1081
|
+
const params = checkParams(rule, paramsFor(def, each ? itemKind : kind), isReservedRuleKey, `Rule "${rule.rule}"`, path, issues);
|
|
1082
|
+
if (!params) return void 0;
|
|
1083
|
+
return {
|
|
1084
|
+
name: rule.rule,
|
|
1085
|
+
def,
|
|
1086
|
+
params,
|
|
1087
|
+
message: rule.message,
|
|
1088
|
+
each
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Checks the params of a rule or a form rule. Every key that isn't reserved must be one of
|
|
1093
|
+
* `params`, and together they must pass it. Returns the parsed params, or undefined when they
|
|
1094
|
+
* have issues. `at` names the rule in messages, e.g. `Rule "minLength"`.
|
|
1095
|
+
*/
|
|
1096
|
+
function checkParams(raw, params, isReserved, at, path, issues) {
|
|
991
1097
|
const issueCount = issues.length;
|
|
992
|
-
const params = paramsFor(def, each ? itemKind : kind);
|
|
993
1098
|
const paramKeys = Object.keys(params?.entries ?? {});
|
|
994
1099
|
const input = {};
|
|
995
|
-
for (const key of Object.keys(
|
|
996
|
-
if (
|
|
997
|
-
if (paramKeys.includes(key)) input[key] =
|
|
998
|
-
else issues.push(issue("unknown_param", [...path, key],
|
|
1100
|
+
for (const key of Object.keys(raw)) {
|
|
1101
|
+
if (isReserved(key)) continue;
|
|
1102
|
+
if (paramKeys.includes(key)) input[key] = raw[key];
|
|
1103
|
+
else issues.push(issue("unknown_param", [...path, key], `${at} has no param "${key}"`));
|
|
999
1104
|
}
|
|
1000
1105
|
const parsed = v.safeParse(params ?? noParams, input);
|
|
1001
1106
|
if (!parsed.success) issues.push(...toIssues("invalid_params", path, parsed.issues));
|
|
1002
1107
|
if (!parsed.success || issues.length > issueCount) return void 0;
|
|
1003
|
-
return
|
|
1004
|
-
name: rule.rule,
|
|
1005
|
-
def,
|
|
1006
|
-
params: parsed.output,
|
|
1007
|
-
message: rule.message,
|
|
1008
|
-
each
|
|
1009
|
-
};
|
|
1108
|
+
return parsed.output;
|
|
1010
1109
|
}
|
|
1011
1110
|
function checkRegistry(registry) {
|
|
1012
1111
|
for (const [name, rule] of Object.entries(registry.rules)) for (const kind of rule.kinds) for (const key of Object.keys(paramsFor(rule, kind)?.entries ?? {})) if (isReservedRuleKey(key)) throw new TypeError(`Rule "${name}": params can't use the reserved key "${key}"`);
|
|
1112
|
+
for (const [name, rule] of Object.entries(registry.formRules)) for (const key of Object.keys(rule.params?.entries ?? {})) if (isReservedFormRuleKey(key)) throw new TypeError(`Form rule "${name}": params can't use the reserved key "${key}"`);
|
|
1013
1113
|
for (const [name, type] of Object.entries(registry.fieldTypes)) {
|
|
1014
1114
|
const at = `Field type "${name}"`;
|
|
1015
1115
|
for (const kind of [type.kind, type.itemKind]) if (typeof kind === "string" && !lookup(registry.kinds, kind)) throw new TypeError(`${at}: kind "${kind}" is not registered`);
|
|
1116
|
+
if (type.container && type.kind !== "object" && type.kind !== "array") throw new TypeError(`${at}: a container's kind must be 'object' or 'array'`);
|
|
1016
1117
|
if (type.kind !== "none" && !type.schema) throw new TypeError(`${at}: schema() is required unless the kind is 'none'`);
|
|
1017
1118
|
for (const key of Object.keys(type.props?.entries ?? {})) if (isReservedFieldKey(key)) throw new TypeError(`${at}: props can't use the reserved key "${key}"`);
|
|
1018
1119
|
if (typeof type.kind === "string" && typeof type.itemKind !== "function") checkDefaultRules(name, type, {
|
|
@@ -1034,6 +1135,293 @@ function toIssues(code, path, valibotIssues) {
|
|
|
1034
1135
|
return issue(code, [...path, ...keys], valibotIssue.message);
|
|
1035
1136
|
});
|
|
1036
1137
|
}
|
|
1138
|
+
// src/builder/check-form-rules.ts
|
|
1139
|
+
const ruleShape = v.looseObject({
|
|
1140
|
+
rule: v.string(),
|
|
1141
|
+
field: v.string(),
|
|
1142
|
+
message: v.string()
|
|
1143
|
+
});
|
|
1144
|
+
/**
|
|
1145
|
+
* Checks the definition's form rules against the fields the compiler built, and adds their
|
|
1146
|
+
* issues to `issues`. Returns the rules that are ready to build.
|
|
1147
|
+
*/
|
|
1148
|
+
function checkFormRules(rules, registry, fieldsByPath, issues) {
|
|
1149
|
+
return rules.flatMap((raw, i) => checkFormRule(raw, ["formRules", i], registry, fieldsByPath, issues) ?? []);
|
|
1150
|
+
}
|
|
1151
|
+
function checkFormRule(raw, path, registry, fieldsByPath, issues) {
|
|
1152
|
+
const shape = v.safeParse(ruleShape, raw);
|
|
1153
|
+
if (!shape.success) {
|
|
1154
|
+
issues.push(...toIssues("invalid_shape", path, shape.issues));
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const rule = shape.output;
|
|
1158
|
+
const at = `Form rule "${rule.rule}"`;
|
|
1159
|
+
const def = lookup(registry.formRules, rule.rule);
|
|
1160
|
+
if (!def) {
|
|
1161
|
+
issues.push(issue("unknown_rule", [...path, "rule"], `Unknown form rule "${rule.rule}"`));
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const params = checkParams(rule, def.params, isReservedFormRuleKey, at, path, issues);
|
|
1165
|
+
if (!params) return void 0;
|
|
1166
|
+
const dotPaths = [rule.field, ...def.reads(params)];
|
|
1167
|
+
const paths = dotPaths.map((dotPath) => dotPath.split("."));
|
|
1168
|
+
const fieldKeys = paths[0];
|
|
1169
|
+
const issueCount = issues.length;
|
|
1170
|
+
const fields = dotPaths.map((dotPath, i) => {
|
|
1171
|
+
const found = findField(dotPath, fieldsByPath);
|
|
1172
|
+
const message = typeof found === "string" ? found : i > 0 && outsideItem(paths[i], fieldKeys);
|
|
1173
|
+
if (!message) return found;
|
|
1174
|
+
issues.push(issue("unknown_field", i === 0 ? [...path, "field"] : path, message));
|
|
1175
|
+
return null;
|
|
1176
|
+
});
|
|
1177
|
+
if (issues.length > issueCount) return void 0;
|
|
1178
|
+
if (fields.some((field) => !field)) return void 0;
|
|
1179
|
+
const checked = fields;
|
|
1180
|
+
const kinds = checked.map((field) => field.valueKind);
|
|
1181
|
+
const wrong = kinds.find((kind) => def.kinds && !def.kinds.includes(kind));
|
|
1182
|
+
if (wrong !== void 0) {
|
|
1183
|
+
const message = `${at} can't be used on ${wrong} fields`;
|
|
1184
|
+
issues.push(issue("kind_mismatch", [...path, "rule"], message));
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (def.sameKind && new Set(kinds).size > 1) {
|
|
1188
|
+
const message = `${at} needs fields of one kind, not ${[...new Set(kinds)].join(" and ")}`;
|
|
1189
|
+
issues.push(issue("kind_mismatch", [...path, "rule"], message));
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const field = checked[0].field;
|
|
1193
|
+
return {
|
|
1194
|
+
name: rule.rule,
|
|
1195
|
+
def,
|
|
1196
|
+
params,
|
|
1197
|
+
message: rule.message,
|
|
1198
|
+
path,
|
|
1199
|
+
field,
|
|
1200
|
+
paths
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* The field with a value at `dotPath`. Null when a field on the way has issues of its own, and
|
|
1205
|
+
* the issue's message when there is no such field.
|
|
1206
|
+
*/
|
|
1207
|
+
function findField(dotPath, fieldsByPath) {
|
|
1208
|
+
const keys = dotPath.split(".");
|
|
1209
|
+
let found;
|
|
1210
|
+
for (let i = 1; i <= keys.length; i++) {
|
|
1211
|
+
found = fieldsByPath.get(keys.slice(0, i).join("."));
|
|
1212
|
+
if (found === null) return null;
|
|
1213
|
+
if (found) continue;
|
|
1214
|
+
if (keys[i - 1] === "*" && i < keys.length) continue;
|
|
1215
|
+
if (fieldsByPath.get(keys.slice(0, i - 1).join("."))?.valueKind === "array" && keys[i - 1] !== "*") return `Unknown field "${dotPath}". A list's items are read with "*": "${[
|
|
1216
|
+
...keys.slice(0, i - 1),
|
|
1217
|
+
"*",
|
|
1218
|
+
...keys.slice(i - 1)
|
|
1219
|
+
].join(".")}"`;
|
|
1220
|
+
return `Unknown field "${dotPath}"`;
|
|
1221
|
+
}
|
|
1222
|
+
return found ?? `Unknown field "${dotPath}"`;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* A path into the items of a list is read once per item, so `field` must be in the same items.
|
|
1226
|
+
* Returns the issue's message when it isn't.
|
|
1227
|
+
*/
|
|
1228
|
+
function outsideItem(keys, fieldKeys) {
|
|
1229
|
+
const star = keys.lastIndexOf("*");
|
|
1230
|
+
const item = (path) => path.slice(0, star + 1).join(".");
|
|
1231
|
+
if (star === -1 || item(keys) === item(fieldKeys)) return void 0;
|
|
1232
|
+
const list = keys.slice(0, star).join(".");
|
|
1233
|
+
return `"${keys.join(".")}" is read from each item of "${list}", so "${fieldKeys.join(".")}" must be too`;
|
|
1234
|
+
}
|
|
1235
|
+
// src/builder/form-rule-action.ts
|
|
1236
|
+
/**
|
|
1237
|
+
* Runs a form rule on the form's value and puts its issue on `field`. A rule on the fields of a
|
|
1238
|
+
* list's items runs once per item, with the item's index in place of each `'*'`.
|
|
1239
|
+
*
|
|
1240
|
+
* The rule only runs when none of the fields it reads has an issue. `v.partialCheck` can't do
|
|
1241
|
+
* this: it runs whenever the form has no schema issues, even when a field it reads failed a rule.
|
|
1242
|
+
*/
|
|
1243
|
+
function formRuleAction(rule, message) {
|
|
1244
|
+
const { def, params, paths } = rule;
|
|
1245
|
+
const fieldKeys = paths[0];
|
|
1246
|
+
return v.rawCheck(({ dataset, addIssue }) => {
|
|
1247
|
+
for (const indexes of itemIndexes(dataset.value, fieldKeys)) {
|
|
1248
|
+
const read = paths.map((keys) => withIndexes(keys, indexes));
|
|
1249
|
+
if (read.some((path) => hasIssue(dataset.issues, path))) continue;
|
|
1250
|
+
const values = read.map((path) => valueAt(dataset.value, path));
|
|
1251
|
+
if (values.includes(void 0) || def.check(values, params)) continue;
|
|
1252
|
+
const field = read[0];
|
|
1253
|
+
addIssue({
|
|
1254
|
+
message,
|
|
1255
|
+
input: values[0],
|
|
1256
|
+
path: issuePath(dataset.value, field)
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
/** Replaces each `'*'` in `keys` with the next index. */
|
|
1262
|
+
const withIndexes = (keys, indexes) => {
|
|
1263
|
+
let next = 0;
|
|
1264
|
+
return keys.map((key) => key === "*" ? indexes[next++] : key);
|
|
1265
|
+
};
|
|
1266
|
+
const valueAt = (input, path) => path.reduce((value, key) => typeof value === "object" && value !== null ? value[key] : void 0, input);
|
|
1267
|
+
/** True when an issue is on `path`, inside it, or on a value that holds it. */
|
|
1268
|
+
const hasIssue = (issues, path) => !!issues?.some((issue) => (issue.path ?? []).every((item, i) => i >= path.length || item.key === path[i]));
|
|
1269
|
+
/** Valibot's path to `path` in `input`, the way an object or array schema reports it. */
|
|
1270
|
+
const issuePath = (input, path) => path.map((key, i) => {
|
|
1271
|
+
const parent = valueAt(input, path.slice(0, i));
|
|
1272
|
+
return {
|
|
1273
|
+
type: Array.isArray(parent) ? "array" : "object",
|
|
1274
|
+
origin: "value",
|
|
1275
|
+
input: parent,
|
|
1276
|
+
key,
|
|
1277
|
+
value: valueAt(parent, [key])
|
|
1278
|
+
};
|
|
1279
|
+
});
|
|
1280
|
+
/** The indexes for each `'*'` in `keys`, for every item `value` has there. */
|
|
1281
|
+
function* itemIndexes(value, keys, indexes = []) {
|
|
1282
|
+
const star = keys.indexOf("*");
|
|
1283
|
+
if (star === -1) {
|
|
1284
|
+
yield indexes;
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const list = valueAt(value, keys.slice(0, star));
|
|
1288
|
+
if (!Array.isArray(list)) return;
|
|
1289
|
+
for (const [i, item] of list.entries()) yield* itemIndexes(item, keys.slice(star + 1), [...indexes, i]);
|
|
1290
|
+
}
|
|
1291
|
+
// src/builder/compile-definition.ts
|
|
1292
|
+
const formShape = v.object({
|
|
1293
|
+
version: v.string(),
|
|
1294
|
+
fields: v.array(v.unknown()),
|
|
1295
|
+
formRules: v.optional(v.array(v.unknown()))
|
|
1296
|
+
});
|
|
1297
|
+
/**
|
|
1298
|
+
* Checks a definition and builds its schema in the same pass, so `validate()` and `build()`
|
|
1299
|
+
* always agree. Never throws on bad input.
|
|
1300
|
+
*/
|
|
1301
|
+
function compileDefinition(json, registry, messages) {
|
|
1302
|
+
const issues = [];
|
|
1303
|
+
const form = v.safeParse(formShape, json);
|
|
1304
|
+
if (!form.success) return {
|
|
1305
|
+
issues: toIssues("invalid_shape", [], form.issues),
|
|
1306
|
+
schema: v.object({})
|
|
1307
|
+
};
|
|
1308
|
+
if (form.output.version !== "1") {
|
|
1309
|
+
const message = `Unsupported version "${form.output.version}". This package reads version '1'.`;
|
|
1310
|
+
issues.push({
|
|
1311
|
+
code: "unsupported_version",
|
|
1312
|
+
path: ["version"],
|
|
1313
|
+
message
|
|
1314
|
+
});
|
|
1315
|
+
return {
|
|
1316
|
+
issues,
|
|
1317
|
+
schema: v.object({})
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
const compiler = createCompiler(registry, messages, issues);
|
|
1321
|
+
const schema = compiler.fields(form.output.fields, ["fields"]);
|
|
1322
|
+
const rules = checkFormRules(form.output.formRules ?? [], registry, compiler.fieldsByPath, issues);
|
|
1323
|
+
if (issues.length || !rules.length) return {
|
|
1324
|
+
issues,
|
|
1325
|
+
schema
|
|
1326
|
+
};
|
|
1327
|
+
return {
|
|
1328
|
+
issues,
|
|
1329
|
+
schema: rules.reduce((acc, rule) => {
|
|
1330
|
+
const ctx = {
|
|
1331
|
+
field: rule.field,
|
|
1332
|
+
rule: rule.name,
|
|
1333
|
+
path: rule.path
|
|
1334
|
+
};
|
|
1335
|
+
return v.pipe(acc, formRuleAction(rule, messages.resolve(rule.message, ctx)));
|
|
1336
|
+
}, schema)
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
/** Every field the compiler checks adds its issues to `issues`. */
|
|
1340
|
+
function createCompiler(registry, messages, issues) {
|
|
1341
|
+
const context = (path) => ({
|
|
1342
|
+
path,
|
|
1343
|
+
buildFields: fields,
|
|
1344
|
+
buildField: field
|
|
1345
|
+
});
|
|
1346
|
+
const fieldsByPath = /* @__PURE__ */ new Map();
|
|
1347
|
+
const parents = [];
|
|
1348
|
+
const parentPath = () => parents.at(-1) ?? [];
|
|
1349
|
+
function fields(list, path) {
|
|
1350
|
+
const entries = {};
|
|
1351
|
+
const names = /* @__PURE__ */ new Set();
|
|
1352
|
+
list.forEach((raw, i) => {
|
|
1353
|
+
const checked = checkField(raw, [...path, i], registry, names, issues);
|
|
1354
|
+
const name = checked ? checked.name : nameOf(raw);
|
|
1355
|
+
if (name === void 0) return;
|
|
1356
|
+
const dataPath = [...parentPath(), name];
|
|
1357
|
+
remember(raw, checked, dataPath);
|
|
1358
|
+
if (checked) entries[name] = build(checked, dataPath);
|
|
1359
|
+
});
|
|
1360
|
+
return v.object(entries);
|
|
1361
|
+
}
|
|
1362
|
+
function field(raw, path) {
|
|
1363
|
+
const checked = checkField(raw, path, registry, void 0, issues);
|
|
1364
|
+
const dataPath = parentPath();
|
|
1365
|
+
remember(raw, checked, dataPath);
|
|
1366
|
+
return checked ? build(checked, dataPath) : v.never();
|
|
1367
|
+
}
|
|
1368
|
+
function build(checked, dataPath) {
|
|
1369
|
+
parents.push(checked.valueKind === "array" ? [...dataPath, "*"] : dataPath);
|
|
1370
|
+
try {
|
|
1371
|
+
const schema = buildFieldSchema(checked, messages, context(checked.path));
|
|
1372
|
+
checkDefault(checked, schema, issues);
|
|
1373
|
+
return schema;
|
|
1374
|
+
} finally {
|
|
1375
|
+
parents.pop();
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/** The first field at a path wins, as with duplicate names. Display elements are left out. */
|
|
1379
|
+
function remember(raw, checked, dataPath) {
|
|
1380
|
+
const key = dataPath.join(".");
|
|
1381
|
+
if (fieldsByPath.has(key) || !checked && !hasValue(raw, registry)) return;
|
|
1382
|
+
fieldsByPath.set(key, checked ?? null);
|
|
1383
|
+
}
|
|
1384
|
+
return {
|
|
1385
|
+
fields,
|
|
1386
|
+
field,
|
|
1387
|
+
fieldsByPath
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
1391
|
+
const nameOf = (raw) => isRecord(raw) && typeof raw.name === "string" ? raw.name : void 0;
|
|
1392
|
+
/** An unknown type counts as one with a value, so a form rule that reads the field is fine. */
|
|
1393
|
+
const hasValue = (raw, registry) => isRecord(raw) && typeof raw.type === "string" && lookup(registry.fieldTypes, raw.type)?.kind !== "none";
|
|
1394
|
+
/** A default must be a value the field accepts. An empty one is the same as none. */
|
|
1395
|
+
function checkDefault({ field, kind, path }, schema, issues) {
|
|
1396
|
+
if (field.default === void 0 || kind.isEmpty(field.default)) return;
|
|
1397
|
+
const result = v.safeParse(schema, field.default);
|
|
1398
|
+
if (!result.success) issues.push(...toIssues("invalid_default", [...path, "default"], result.issues));
|
|
1399
|
+
}
|
|
1400
|
+
// src/builder/build-context.ts
|
|
1401
|
+
const messages = {
|
|
1402
|
+
required: "Required",
|
|
1403
|
+
resolve: (text) => text
|
|
1404
|
+
};
|
|
1405
|
+
/**
|
|
1406
|
+
* A build context for testing a field type on its own, without a definition around it.
|
|
1407
|
+
* `buildFields` and `buildField` use the default registries and throw a `DefinitionError` when
|
|
1408
|
+
* the child fields have issues. `overrides` replaces any member, e.g. `path`.
|
|
1409
|
+
*/
|
|
1410
|
+
function createBuildContext(overrides = {}) {
|
|
1411
|
+
const registry = mergeRegistry({});
|
|
1412
|
+
const run = (compile) => {
|
|
1413
|
+
const issues = [];
|
|
1414
|
+
const result = compile(createCompiler(registry, messages, issues));
|
|
1415
|
+
if (issues.length) throw new DefinitionError(issues);
|
|
1416
|
+
return result;
|
|
1417
|
+
};
|
|
1418
|
+
return {
|
|
1419
|
+
path: [],
|
|
1420
|
+
buildFields: (fields, path) => run((compiler) => compiler.fields(fields, path)),
|
|
1421
|
+
buildField: (field, path) => run((compiler) => compiler.field(field, path)),
|
|
1422
|
+
...overrides
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1037
1425
|
// src/builder/create-schema-builder.ts
|
|
1038
1426
|
function createSchemaBuilder(options = {}) {
|
|
1039
1427
|
const registry = mergeRegistry(options);
|
|
@@ -1044,7 +1432,7 @@ function createSchemaBuilder(options = {}) {
|
|
|
1044
1432
|
};
|
|
1045
1433
|
/** Every issue in the definition, in document order. Never throws on bad input. */
|
|
1046
1434
|
function validate(json) {
|
|
1047
|
-
const { issues } =
|
|
1435
|
+
const { issues } = compileDefinition(json, registry, messages);
|
|
1048
1436
|
return issues.length ? {
|
|
1049
1437
|
success: false,
|
|
1050
1438
|
issues
|
|
@@ -1054,11 +1442,9 @@ function createSchemaBuilder(options = {}) {
|
|
|
1054
1442
|
};
|
|
1055
1443
|
}
|
|
1056
1444
|
function build(json) {
|
|
1057
|
-
const { issues,
|
|
1445
|
+
const { issues, schema } = compileDefinition(json, registry, messages);
|
|
1058
1446
|
if (issues.length) throw new DefinitionError(issues);
|
|
1059
|
-
|
|
1060
|
-
for (const field of fields) entries[field.name] = buildFieldSchema(field, messages);
|
|
1061
|
-
return v.object(entries);
|
|
1447
|
+
return schema;
|
|
1062
1448
|
}
|
|
1063
1449
|
/**
|
|
1064
1450
|
* The registered rules that can be used on a field of `kind`, e.g. for a rule picker.
|
|
@@ -1072,9 +1458,66 @@ function createSchemaBuilder(options = {}) {
|
|
|
1072
1458
|
rulesForKind,
|
|
1073
1459
|
kinds: registry.kinds,
|
|
1074
1460
|
fieldTypes: registry.fieldTypes,
|
|
1075
|
-
rules: registry.rules
|
|
1461
|
+
rules: registry.rules,
|
|
1462
|
+
formRules: registry.formRules
|
|
1076
1463
|
};
|
|
1077
1464
|
}
|
|
1078
|
-
|
|
1465
|
+
// src/definition/initial-input.ts
|
|
1466
|
+
/**
|
|
1467
|
+
* Formisch's `initialInput` for a definition: each field's `default`, nested the way the value
|
|
1468
|
+
* is. A group gives an object, and an array without a default gives `[]`. Other fields without
|
|
1469
|
+
* a default are left out. Like `walkFields`, it reads the built-in `group` and `array` types
|
|
1470
|
+
* and needs no builder.
|
|
1471
|
+
*/
|
|
1472
|
+
function getInitialInput(definition) {
|
|
1473
|
+
return inputOf(definition.fields);
|
|
1474
|
+
}
|
|
1475
|
+
function inputOf(fields) {
|
|
1476
|
+
const input = {};
|
|
1477
|
+
for (const field of fields) {
|
|
1478
|
+
if (field.name === void 0) continue;
|
|
1479
|
+
const value = initialValue(field);
|
|
1480
|
+
if (value !== void 0) input[field.name] = value;
|
|
1481
|
+
}
|
|
1482
|
+
return input;
|
|
1483
|
+
}
|
|
1484
|
+
function initialValue(field) {
|
|
1485
|
+
if (field.type === "group") return inputOf(Array.isArray(field.fields) ? field.fields : []);
|
|
1486
|
+
if (field.type === "array") return field.default ?? [];
|
|
1487
|
+
return field.default;
|
|
1488
|
+
}
|
|
1489
|
+
// src/definition/walk-fields.ts
|
|
1490
|
+
/**
|
|
1491
|
+
* Every field of a definition, at every depth, in document order. A group or an array comes
|
|
1492
|
+
* before its fields, and the fields of an array's items come once. It reads the built-in
|
|
1493
|
+
* `group` and `array` types and needs no builder, so it doesn't check the definition: run
|
|
1494
|
+
* `validate()` first.
|
|
1495
|
+
*/
|
|
1496
|
+
function* walkFields(definition) {
|
|
1497
|
+
yield* walkList(definition.fields, ["fields"], [], 0);
|
|
1498
|
+
}
|
|
1499
|
+
function* walkList(fields, at, dataPath, depth) {
|
|
1500
|
+
for (const [i, field] of fields.entries()) {
|
|
1501
|
+
const path = field.name === void 0 ? dataPath : [...dataPath, field.name];
|
|
1502
|
+
yield* walkField(field, [...at, i], path, depth);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
function* walkField(field, definitionPath, dataPath, depth) {
|
|
1506
|
+
yield {
|
|
1507
|
+
field,
|
|
1508
|
+
type: field.type,
|
|
1509
|
+
dataPath,
|
|
1510
|
+
definitionPath,
|
|
1511
|
+
depth
|
|
1512
|
+
};
|
|
1513
|
+
if (field.type === "group" && Array.isArray(field.fields)) yield* walkList(field.fields, [...definitionPath, "fields"], dataPath, depth + 1);
|
|
1514
|
+
else if (field.type === "array") {
|
|
1515
|
+
const items = [...dataPath, "*"];
|
|
1516
|
+
if (isField(field.of)) yield* walkField(field.of, [...definitionPath, "of"], items, depth + 1);
|
|
1517
|
+
else if (Array.isArray(field.fields)) yield* walkList(field.fields, [...definitionPath, "fields"], items, depth + 1);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
const isField = (value) => typeof value === "object" && value !== null && "type" in value;
|
|
1521
|
+
export { DefinitionError, RESERVED_FIELD_KEYS, RESERVED_FORM_RULE_KEYS, RESERVED_RULE_KEYS, comparisonRules, contentRules, createBuildContext, createSchemaBuilder, dateRules, defaultFieldTypes, defaultFormRules, defaultKinds, defaultRules, defineFieldType, defineFormRule, defineRule, fileRules, formatPath, formatRules, getInitialInput, lengthRules, normalizeRules, numberRules, textUnitRules, walkFields };
|
|
1079
1522
|
|
|
1080
1523
|
//# sourceMappingURL=index.js.map
|