@whatsaaaa/form-builder-schema 0.2.0 → 0.4.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 +103 -8
- package/dist/index.d.ts +497 -18
- package/dist/index.js +745 -68
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
+
// src/shared/each-item.ts
|
|
3
|
+
/**
|
|
4
|
+
* Checks every item of an array with `schema` and reports the issues on the array itself.
|
|
5
|
+
* Formisch keeps no field for each item of an array value, so an issue with an item's index in
|
|
6
|
+
* its path would show nowhere.
|
|
7
|
+
*/
|
|
8
|
+
function eachItem(schema) {
|
|
9
|
+
return v.rawCheck(({ dataset, config, addIssue }) => {
|
|
10
|
+
if (!dataset.typed || !Array.isArray(dataset.value)) return;
|
|
11
|
+
for (const item of dataset.value) {
|
|
12
|
+
const result = v.safeParse(schema, item, config);
|
|
13
|
+
for (const issue of result.issues ?? []) addIssue({
|
|
14
|
+
message: issue.message,
|
|
15
|
+
input: item,
|
|
16
|
+
expected: issue.expected ?? void 0,
|
|
17
|
+
received: issue.received
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
2
22
|
// src/builder/build-field-schema.ts
|
|
3
23
|
/**
|
|
4
24
|
* The schema runs in this order: empty check, base schema, normalize rules, required check,
|
|
5
|
-
* then the type's default rules and the field's rules
|
|
25
|
+
* then the type's default rules and the field's rules, and last the type's `finalize()`.
|
|
6
26
|
*/
|
|
7
27
|
function buildFieldSchema(checked, messages) {
|
|
8
28
|
const { field, path, type, kind } = checked;
|
|
@@ -11,8 +31,14 @@ function buildFieldSchema(checked, messages) {
|
|
|
11
31
|
field,
|
|
12
32
|
path
|
|
13
33
|
});
|
|
14
|
-
const
|
|
15
|
-
|
|
34
|
+
const whole = {
|
|
35
|
+
normalize: [],
|
|
36
|
+
validate: []
|
|
37
|
+
};
|
|
38
|
+
const each = {
|
|
39
|
+
normalize: [],
|
|
40
|
+
validate: []
|
|
41
|
+
};
|
|
16
42
|
for (const rule of checked.rules) {
|
|
17
43
|
const message = rule.message === void 0 ? void 0 : messages.resolve(rule.message, {
|
|
18
44
|
field,
|
|
@@ -20,25 +46,31 @@ function buildFieldSchema(checked, messages) {
|
|
|
20
46
|
path
|
|
21
47
|
});
|
|
22
48
|
const action = rule.def.create(rule.params, message);
|
|
23
|
-
|
|
24
|
-
|
|
49
|
+
const stage = rule.def.stage ?? "validate";
|
|
50
|
+
if (rule.each) each[stage].push(action);
|
|
51
|
+
else whole[stage].push(action);
|
|
25
52
|
}
|
|
53
|
+
if (each.normalize.length) whole.normalize.unshift(v.array(pipe(v.unknown(), each.normalize)));
|
|
54
|
+
if (each.validate.length) whole.validate.unshift(eachItem(pipe(v.unknown(), each.validate)));
|
|
55
|
+
const finalize = type.finalize?.(field);
|
|
56
|
+
if (finalize) whole.validate.push(finalize);
|
|
26
57
|
const base = type.schema(field, { path });
|
|
27
|
-
const requiredCheck = required ?
|
|
28
|
-
const head = pipe(base, requiredCheck ? [...normalize, requiredCheck] : normalize);
|
|
58
|
+
const requiredCheck = !required ? void 0 : type.required ? type.required(requiredMessage) : kind.required?.(requiredMessage);
|
|
59
|
+
const head = pipe(base, requiredCheck ? [...whole.normalize, requiredCheck] : whole.normalize);
|
|
29
60
|
const empty = required ? v.never(requiredMessage) : v.pipe(v.unknown(), v.transform(() => void 0));
|
|
30
61
|
const whenFilled = (schema) => v.lazy((input) => kind.isEmpty(input) ? empty : schema);
|
|
31
|
-
const schema = whenFilled(v.pipe(head, whenFilled(pipe(v.unknown(), validate))));
|
|
62
|
+
const schema = whenFilled(v.pipe(head, whenFilled(pipe(v.unknown(), whole.validate))));
|
|
32
63
|
return required ? schema : v.optional(schema);
|
|
33
64
|
}
|
|
34
|
-
const pipe = (schema,
|
|
35
|
-
// src/field-types/
|
|
36
|
-
|
|
65
|
+
const pipe = (schema, items) => items.reduce((acc, item) => v.pipe(acc, item), schema);
|
|
66
|
+
// src/field-types/basic-types.ts
|
|
67
|
+
/** Types with one plain value and no props. Each maps to a native input. */
|
|
68
|
+
const basicTypes = {
|
|
37
69
|
text: {
|
|
38
70
|
kind: "string",
|
|
39
71
|
schema: () => v.string()
|
|
40
72
|
},
|
|
41
|
-
|
|
73
|
+
textarea: {
|
|
42
74
|
kind: "string",
|
|
43
75
|
schema: () => v.string()
|
|
44
76
|
},
|
|
@@ -47,10 +79,149 @@ const defaultFieldTypes = {
|
|
|
47
79
|
schema: () => v.string(),
|
|
48
80
|
defaultRules: [{ rule: "email" }]
|
|
49
81
|
},
|
|
82
|
+
password: {
|
|
83
|
+
kind: "string",
|
|
84
|
+
schema: () => v.string()
|
|
85
|
+
},
|
|
86
|
+
url: {
|
|
87
|
+
kind: "string",
|
|
88
|
+
schema: () => v.string(),
|
|
89
|
+
defaultRules: [{ rule: "url" }]
|
|
90
|
+
},
|
|
91
|
+
tel: {
|
|
92
|
+
kind: "string",
|
|
93
|
+
schema: () => v.string()
|
|
94
|
+
},
|
|
50
95
|
number: {
|
|
51
96
|
kind: "number",
|
|
52
97
|
schema: () => v.number()
|
|
98
|
+
},
|
|
99
|
+
checkbox: {
|
|
100
|
+
kind: "boolean",
|
|
101
|
+
schema: () => v.boolean()
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
// src/field-types/define-field-type.ts
|
|
105
|
+
const defineFieldType = (def) => def;
|
|
106
|
+
// src/field-types/choice-types.ts
|
|
107
|
+
const options = (value) => v.pipe(v.array(v.object({
|
|
108
|
+
value,
|
|
109
|
+
label: v.string()
|
|
110
|
+
})), v.minLength(1, "Give at least one option"), v.check((items) => new Set(items.map((item) => typeof item.value)).size <= 1, "Give every option a value of the same type"), v.check((items) => new Set(items.map((item) => item.value)).size === items.length, "Give every option a different value"));
|
|
111
|
+
const stringOrNumber = v.union([v.string(), v.number()]);
|
|
112
|
+
const anyValue = v.union([
|
|
113
|
+
v.string(),
|
|
114
|
+
v.number(),
|
|
115
|
+
v.boolean()
|
|
116
|
+
]);
|
|
117
|
+
/** The options' value type. Every option has the same one. */
|
|
118
|
+
const kindOf = (items) => {
|
|
119
|
+
const [first] = items;
|
|
120
|
+
return first ? typeof first.value : "string";
|
|
121
|
+
};
|
|
122
|
+
/** One of the option values. `v.picklist` can't hold booleans, so this is a union of literals. */
|
|
123
|
+
const oneOf = (items) => v.union(items.map((item) => v.literal(item.value)));
|
|
124
|
+
const choiceTypes = {
|
|
125
|
+
select: defineFieldType({
|
|
126
|
+
kind: (field) => field.options ? kindOf(field.options) : field.valueType ?? "string",
|
|
127
|
+
props: v.object({
|
|
128
|
+
options: v.optional(options(stringOrNumber)),
|
|
129
|
+
/** The value's type while `options` is missing, e.g. until they load from an API. */
|
|
130
|
+
valueType: v.optional(v.picklist(["string", "number"]))
|
|
131
|
+
}),
|
|
132
|
+
schema: (field) => {
|
|
133
|
+
if (field.options) return oneOf(field.options);
|
|
134
|
+
return field.valueType === "number" ? v.number() : v.string();
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
137
|
+
radio: defineFieldType({
|
|
138
|
+
kind: (field) => kindOf(field.options),
|
|
139
|
+
props: v.object({ options: options(anyValue) }),
|
|
140
|
+
schema: (field) => oneOf(field.options),
|
|
141
|
+
required: () => null
|
|
142
|
+
}),
|
|
143
|
+
multiselect: defineFieldType({
|
|
144
|
+
kind: "array",
|
|
145
|
+
itemKind: (field) => kindOf(field.options),
|
|
146
|
+
props: v.object({ options: options(stringOrNumber) }),
|
|
147
|
+
schema: (field) => v.array(oneOf(field.options))
|
|
148
|
+
})
|
|
149
|
+
};
|
|
150
|
+
// src/field-types/date-types.ts
|
|
151
|
+
const outputProps = v.object({
|
|
152
|
+
/** `'date'` turns the ISO string into a `Date`. Defaults to `'string'`. */
|
|
153
|
+
output: v.optional(v.picklist(["string", "date"])) });
|
|
154
|
+
const finalize = (field) => field.output === "date" ? v.toDate() : void 0;
|
|
155
|
+
/**
|
|
156
|
+
* Dates and times are ISO strings, the values of <input type="date|time|datetime-local">.
|
|
157
|
+
* The format is part of the type rather than a default rule, so these types don't depend on
|
|
158
|
+
* `dateRules` being registered.
|
|
159
|
+
*/
|
|
160
|
+
const dateTypes = {
|
|
161
|
+
date: defineFieldType({
|
|
162
|
+
kind: "date",
|
|
163
|
+
props: outputProps,
|
|
164
|
+
schema: () => v.pipe(v.string(), v.isoDate()),
|
|
165
|
+
finalize
|
|
166
|
+
}),
|
|
167
|
+
time: {
|
|
168
|
+
kind: "date",
|
|
169
|
+
schema: () => v.pipe(v.string(), v.isoTime())
|
|
170
|
+
},
|
|
171
|
+
datetime: defineFieldType({
|
|
172
|
+
kind: "date",
|
|
173
|
+
props: outputProps,
|
|
174
|
+
schema: () => v.pipe(v.string(), v.isoDateTime()),
|
|
175
|
+
finalize
|
|
176
|
+
})
|
|
177
|
+
};
|
|
178
|
+
// src/field-types/display-types.ts
|
|
179
|
+
/**
|
|
180
|
+
* Elements without a value. They need no name and add no output key. Their `text` can be an
|
|
181
|
+
* i18n key: renderers resolve it with the same function they pass as `resolveMessage`.
|
|
182
|
+
*/
|
|
183
|
+
const displayTypes = {
|
|
184
|
+
heading: defineFieldType({
|
|
185
|
+
kind: "none",
|
|
186
|
+
props: v.object({
|
|
187
|
+
text: v.string(),
|
|
188
|
+
/** 1 to 6. Defaults to 2. */
|
|
189
|
+
level: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(6)))
|
|
190
|
+
})
|
|
191
|
+
}),
|
|
192
|
+
paragraph: defineFieldType({
|
|
193
|
+
kind: "none",
|
|
194
|
+
props: v.object({ text: v.string() })
|
|
195
|
+
}),
|
|
196
|
+
divider: { kind: "none" }
|
|
197
|
+
};
|
|
198
|
+
// src/field-types/file-type.ts
|
|
199
|
+
const mimeTypes = (accept) => accept.split(",").map((type) => type.trim());
|
|
200
|
+
/**
|
|
201
|
+
* A File, or an array of them with `multiple`. Rules for files, like `maxSize`, run on each one;
|
|
202
|
+
* rules for arrays, like `maxLength`, limit the count.
|
|
203
|
+
*/
|
|
204
|
+
const fileType = defineFieldType({
|
|
205
|
+
kind: (field) => field.multiple ? "array" : "file",
|
|
206
|
+
itemKind: "file",
|
|
207
|
+
props: v.object({
|
|
208
|
+
multiple: v.optional(v.boolean()),
|
|
209
|
+
accept: v.optional(v.pipe(v.string(), v.check((input) => mimeTypes(input).every((type) => /^[\w.+-]+\/[\w.+-]+$/.test(type)), "Use MIME types separated by commas, such as image/png,image/jpeg")))
|
|
210
|
+
}),
|
|
211
|
+
schema: (field) => {
|
|
212
|
+
const file = field.accept ? v.pipe(v.file(), v.mimeType(mimeTypes(field.accept))) : v.file();
|
|
213
|
+
if (!field.multiple) return file;
|
|
214
|
+
return field.accept ? v.pipe(v.array(v.file()), eachItem(file)) : v.array(file);
|
|
53
215
|
}
|
|
216
|
+
});
|
|
217
|
+
// src/field-types/default-field-types.ts
|
|
218
|
+
/** Every built-in field type. */
|
|
219
|
+
const defaultFieldTypes = {
|
|
220
|
+
...basicTypes,
|
|
221
|
+
...dateTypes,
|
|
222
|
+
...choiceTypes,
|
|
223
|
+
file: fileType,
|
|
224
|
+
...displayTypes
|
|
54
225
|
};
|
|
55
226
|
// src/kinds/default-kinds.ts
|
|
56
227
|
const isNil = (input) => input === void 0 || input === null;
|
|
@@ -78,58 +249,514 @@ const defaultKinds = {
|
|
|
78
249
|
};
|
|
79
250
|
// src/rules/define-rule.ts
|
|
80
251
|
const defineRule = (def) => def;
|
|
81
|
-
// src/rules/
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
252
|
+
// src/rules/param-schemas.ts
|
|
253
|
+
/** A length, count or size: a whole number from 0. */
|
|
254
|
+
const count = v.pipe(v.number(), v.integer(), v.minValue(0));
|
|
255
|
+
const countParams = v.object({ value: count });
|
|
256
|
+
const isIsoValue = (input) => v.ISO_DATE_REGEX.test(input) || v.ISO_DATE_TIME_REGEX.test(input) || v.ISO_TIME_REGEX.test(input);
|
|
257
|
+
/** Valibot compares dates as ISO strings, so the param must be one too. */
|
|
258
|
+
const isoValue = v.pipe(v.string(), v.check(isIsoValue, "Use an ISO date, date-time or time, such as 2026-01-01"));
|
|
259
|
+
const valueSchema = (kind) => {
|
|
260
|
+
switch (kind) {
|
|
261
|
+
case "date": return isoValue;
|
|
262
|
+
case "string": return v.string();
|
|
263
|
+
case "boolean": return v.boolean();
|
|
264
|
+
default: return v.number();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
// src/rules/comparison-rules.ts
|
|
268
|
+
const ordered = ["number", "date"];
|
|
269
|
+
const any = [
|
|
270
|
+
"number",
|
|
271
|
+
"date",
|
|
272
|
+
"string",
|
|
273
|
+
"boolean"
|
|
274
|
+
];
|
|
275
|
+
const meta$6 = (label) => ({
|
|
276
|
+
label,
|
|
277
|
+
category: "comparison"
|
|
278
|
+
});
|
|
279
|
+
const valueParams = (kind) => v.object({ value: valueSchema(kind) });
|
|
280
|
+
const valuesParams = (kind) => v.object({ values: v.pipe(v.array(valueSchema(kind)), v.minLength(1, "Give at least one value")) });
|
|
281
|
+
/** Compares the value with a param of the same type. */
|
|
282
|
+
const comparisonRules = {
|
|
283
|
+
minValue: defineRule({
|
|
284
|
+
kinds: ordered,
|
|
285
|
+
params: valueParams,
|
|
286
|
+
meta: meta$6("Minimum"),
|
|
287
|
+
create: (p, m) => v.minValue(p.value, m)
|
|
288
|
+
}),
|
|
289
|
+
maxValue: defineRule({
|
|
290
|
+
kinds: ordered,
|
|
291
|
+
params: valueParams,
|
|
292
|
+
meta: meta$6("Maximum"),
|
|
293
|
+
create: (p, m) => v.maxValue(p.value, m)
|
|
294
|
+
}),
|
|
295
|
+
gtValue: defineRule({
|
|
296
|
+
kinds: ordered,
|
|
297
|
+
params: valueParams,
|
|
298
|
+
meta: meta$6("Greater than"),
|
|
299
|
+
create: (p, m) => v.gtValue(p.value, m)
|
|
300
|
+
}),
|
|
301
|
+
ltValue: defineRule({
|
|
302
|
+
kinds: ordered,
|
|
303
|
+
params: valueParams,
|
|
304
|
+
meta: meta$6("Less than"),
|
|
305
|
+
create: (p, m) => v.ltValue(p.value, m)
|
|
306
|
+
}),
|
|
307
|
+
value: defineRule({
|
|
308
|
+
kinds: any,
|
|
309
|
+
params: valueParams,
|
|
310
|
+
meta: meta$6("Equals"),
|
|
311
|
+
create: (p, m) => v.value(p.value, m)
|
|
312
|
+
}),
|
|
313
|
+
notValue: defineRule({
|
|
314
|
+
kinds: any,
|
|
315
|
+
params: valueParams,
|
|
316
|
+
meta: meta$6("Does not equal"),
|
|
317
|
+
create: (p, m) => v.notValue(p.value, m)
|
|
318
|
+
}),
|
|
319
|
+
values: defineRule({
|
|
320
|
+
kinds: ordered,
|
|
321
|
+
params: valuesParams,
|
|
322
|
+
meta: meta$6("One of"),
|
|
323
|
+
create: (p, m) => v.values(p.values, m)
|
|
324
|
+
}),
|
|
325
|
+
notValues: defineRule({
|
|
326
|
+
kinds: ordered,
|
|
327
|
+
params: valuesParams,
|
|
328
|
+
meta: meta$6("None of"),
|
|
329
|
+
create: (p, m) => v.notValues(p.values, m)
|
|
330
|
+
})
|
|
331
|
+
};
|
|
332
|
+
// src/rules/content-rules.ts
|
|
333
|
+
const meta$5 = (label, description) => ({
|
|
334
|
+
label,
|
|
335
|
+
description,
|
|
336
|
+
category: "content"
|
|
337
|
+
});
|
|
338
|
+
const regexError = (pattern, flags = "") => {
|
|
339
|
+
try {
|
|
340
|
+
new RegExp(pattern, flags);
|
|
341
|
+
return;
|
|
342
|
+
} catch (error) {
|
|
343
|
+
return error.message;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const FLAGS = /^[dgimsuvy]*$/;
|
|
347
|
+
const regexParams = v.pipe(v.object({
|
|
348
|
+
pattern: v.string(),
|
|
349
|
+
flags: v.optional(v.pipe(v.string(), v.regex(FLAGS, "Flags can be d, g, i, m, s, u, v and y")))
|
|
350
|
+
}), v.forward(v.check((p) => !FLAGS.test(p.flags ?? "") || regexError(p.pattern, p.flags) === void 0, (issue) => regexError(issue.input.pattern, issue.input.flags) ?? "Invalid pattern"), ["pattern"]));
|
|
351
|
+
const item = v.union([
|
|
352
|
+
v.string(),
|
|
353
|
+
v.number(),
|
|
354
|
+
v.boolean()
|
|
355
|
+
]);
|
|
356
|
+
const stringParams = v.object({ value: v.string() });
|
|
357
|
+
const contentParams = (kind) => v.object({ value: kind === "array" ? item : v.string() });
|
|
358
|
+
const contentRules = {
|
|
359
|
+
regex: defineRule({
|
|
85
360
|
kinds: ["string"],
|
|
86
|
-
|
|
87
|
-
|
|
361
|
+
params: regexParams,
|
|
362
|
+
meta: meta$5("Matches pattern", "A JavaScript regular expression, without the slashes."),
|
|
363
|
+
create: (p, m) => v.regex(new RegExp(p.pattern, p.flags?.replace(/[gy]/g, "")), m)
|
|
88
364
|
}),
|
|
89
|
-
|
|
365
|
+
startsWith: defineRule({
|
|
366
|
+
kinds: ["string"],
|
|
367
|
+
params: stringParams,
|
|
368
|
+
meta: meta$5("Starts with"),
|
|
369
|
+
create: (p, m) => v.startsWith(p.value, m)
|
|
370
|
+
}),
|
|
371
|
+
endsWith: defineRule({
|
|
90
372
|
kinds: ["string"],
|
|
91
|
-
params:
|
|
373
|
+
params: stringParams,
|
|
374
|
+
meta: meta$5("Ends with"),
|
|
375
|
+
create: (p, m) => v.endsWith(p.value, m)
|
|
376
|
+
}),
|
|
377
|
+
includes: defineRule({
|
|
378
|
+
kinds: ["string", "array"],
|
|
379
|
+
params: contentParams,
|
|
380
|
+
meta: meta$5("Contains"),
|
|
381
|
+
create: (p, m) => v.includes(p.value, m)
|
|
382
|
+
}),
|
|
383
|
+
excludes: defineRule({
|
|
384
|
+
kinds: ["string", "array"],
|
|
385
|
+
params: contentParams,
|
|
386
|
+
meta: meta$5("Does not contain"),
|
|
387
|
+
create: (p, m) => v.excludes(p.value, m)
|
|
388
|
+
})
|
|
389
|
+
};
|
|
390
|
+
// src/rules/date-rules.ts
|
|
391
|
+
const iso = (label, description, create) => defineRule({
|
|
392
|
+
kinds: ["string", "date"],
|
|
393
|
+
meta: {
|
|
394
|
+
label,
|
|
395
|
+
description,
|
|
396
|
+
category: "date"
|
|
397
|
+
},
|
|
398
|
+
create: (_, message) => create(message)
|
|
399
|
+
});
|
|
400
|
+
const dateRules = {
|
|
401
|
+
isoDate: iso("ISO date", "Like 2026-10-28", (m) => v.isoDate(m)),
|
|
402
|
+
isoDateTime: iso("ISO date and time", "Like 2026-10-28T14:30", (m) => v.isoDateTime(m)),
|
|
403
|
+
isoTime: iso("ISO time", "Like 14:30", (m) => v.isoTime(m)),
|
|
404
|
+
isoTimeSecond: iso("ISO time with seconds", "Like 14:30:15", (m) => v.isoTimeSecond(m)),
|
|
405
|
+
isoTimestamp: iso("ISO timestamp", "Like 2026-10-28T14:30:15.000z", (m) => v.isoTimestamp(m)),
|
|
406
|
+
isoWeek: iso("ISO week", "Like 2026-W44", (m) => v.isoWeek(m))
|
|
407
|
+
};
|
|
408
|
+
// src/rules/file-rules.ts
|
|
409
|
+
const kinds$3 = ["file"];
|
|
410
|
+
const meta$4 = (label, description) => ({
|
|
411
|
+
label,
|
|
412
|
+
description,
|
|
413
|
+
category: "file"
|
|
414
|
+
});
|
|
415
|
+
const bytes = "In bytes, e.g. 1048576 for 1 MiB.";
|
|
416
|
+
const mimeTypeParams = v.object({ types: v.pipe(v.array(v.pipe(v.string(), v.regex(/^[\w.+-]+\/[\w.+-]+$/, "Use a MIME type, such as image/png"))), v.minLength(1, "Give at least one MIME type")) });
|
|
417
|
+
/** Size and type of a File or Blob. */
|
|
418
|
+
const fileRules = {
|
|
419
|
+
minSize: defineRule({
|
|
420
|
+
kinds: kinds$3,
|
|
421
|
+
params: countParams,
|
|
422
|
+
meta: meta$4("Minimum size", bytes),
|
|
423
|
+
create: (p, m) => v.minSize(p.value, m)
|
|
424
|
+
}),
|
|
425
|
+
maxSize: defineRule({
|
|
426
|
+
kinds: kinds$3,
|
|
427
|
+
params: countParams,
|
|
428
|
+
meta: meta$4("Maximum size", bytes),
|
|
429
|
+
create: (p, m) => v.maxSize(p.value, m)
|
|
430
|
+
}),
|
|
431
|
+
size: defineRule({
|
|
432
|
+
kinds: kinds$3,
|
|
433
|
+
params: countParams,
|
|
434
|
+
meta: meta$4("Exact size", bytes),
|
|
435
|
+
create: (p, m) => v.size(p.value, m)
|
|
436
|
+
}),
|
|
437
|
+
notSize: defineRule({
|
|
438
|
+
kinds: kinds$3,
|
|
439
|
+
params: countParams,
|
|
440
|
+
meta: meta$4("Not this size", bytes),
|
|
441
|
+
create: (p, m) => v.notSize(p.value, m)
|
|
442
|
+
}),
|
|
443
|
+
mimeType: defineRule({
|
|
444
|
+
kinds: kinds$3,
|
|
445
|
+
params: mimeTypeParams,
|
|
446
|
+
meta: meta$4("File type"),
|
|
447
|
+
create: (p, m) => v.mimeType(p.types, m)
|
|
448
|
+
})
|
|
449
|
+
};
|
|
450
|
+
// src/rules/format-rules.ts
|
|
451
|
+
/** A string format without params. */
|
|
452
|
+
const format = (label, create) => defineRule({
|
|
453
|
+
kinds: ["string"],
|
|
454
|
+
meta: {
|
|
455
|
+
label,
|
|
456
|
+
category: "format"
|
|
457
|
+
},
|
|
458
|
+
create: (_, message) => create(message)
|
|
459
|
+
});
|
|
460
|
+
const hashParams = v.object({ types: v.pipe(v.array(v.picklist([
|
|
461
|
+
"md4",
|
|
462
|
+
"md5",
|
|
463
|
+
"sha1",
|
|
464
|
+
"sha256",
|
|
465
|
+
"sha384",
|
|
466
|
+
"sha512",
|
|
467
|
+
"ripemd128",
|
|
468
|
+
"ripemd160",
|
|
469
|
+
"tiger128",
|
|
470
|
+
"tiger160",
|
|
471
|
+
"tiger192",
|
|
472
|
+
"crc32",
|
|
473
|
+
"crc32b",
|
|
474
|
+
"adler32"
|
|
475
|
+
])), v.minLength(1, "Pick at least one hash type")) });
|
|
476
|
+
/** Formats Valibot can check on a string. */
|
|
477
|
+
const formatRules = {
|
|
478
|
+
email: format("Email", (m) => v.email(m)),
|
|
479
|
+
rfcEmail: format("Email (RFC 5322)", (m) => v.rfcEmail(m)),
|
|
480
|
+
url: format("URL", (m) => v.url(m)),
|
|
481
|
+
domain: format("Domain", (m) => v.domain(m)),
|
|
482
|
+
slug: format("Slug", (m) => v.slug(m)),
|
|
483
|
+
uuid: format("UUID", (m) => v.uuid(m)),
|
|
484
|
+
ulid: format("ULID", (m) => v.ulid(m)),
|
|
485
|
+
cuid2: format("CUID2", (m) => v.cuid2(m)),
|
|
486
|
+
nanoid: format("Nano ID", (m) => v.nanoid(m)),
|
|
487
|
+
ip: format("IP address", (m) => v.ip(m)),
|
|
488
|
+
ipv4: format("IPv4 address", (m) => v.ipv4(m)),
|
|
489
|
+
ipv6: format("IPv6 address", (m) => v.ipv6(m)),
|
|
490
|
+
mac: format("MAC address", (m) => v.mac(m)),
|
|
491
|
+
mac48: format("MAC-48 address", (m) => v.mac48(m)),
|
|
492
|
+
mac64: format("MAC-64 address", (m) => v.mac64(m)),
|
|
493
|
+
hexColor: format("Hex colour", (m) => v.hexColor(m)),
|
|
494
|
+
hexadecimal: format("Hexadecimal", (m) => v.hexadecimal(m)),
|
|
495
|
+
octal: format("Octal", (m) => v.octal(m)),
|
|
496
|
+
decimal: format("Decimal number", (m) => v.decimal(m)),
|
|
497
|
+
digits: format("Digits only", (m) => v.digits(m)),
|
|
498
|
+
base64: format("Base64", (m) => v.base64(m)),
|
|
499
|
+
bic: format("BIC (SWIFT code)", (m) => v.bic(m)),
|
|
500
|
+
creditCard: format("Credit card number", (m) => v.creditCard(m)),
|
|
501
|
+
imei: format("IMEI", (m) => v.imei(m)),
|
|
502
|
+
isbn: format("ISBN", (m) => v.isbn(m)),
|
|
503
|
+
isrc: format("ISRC", (m) => v.isrc(m)),
|
|
504
|
+
emoji: format("Emoji", (m) => v.emoji(m)),
|
|
505
|
+
hash: defineRule({
|
|
506
|
+
kinds: ["string"],
|
|
507
|
+
params: hashParams,
|
|
508
|
+
meta: {
|
|
509
|
+
label: "Hash",
|
|
510
|
+
category: "format"
|
|
511
|
+
},
|
|
512
|
+
create: (p, m) => v.hash(p.types, m)
|
|
513
|
+
})
|
|
514
|
+
};
|
|
515
|
+
// src/rules/length-rules.ts
|
|
516
|
+
const kinds$2 = ["string", "array"];
|
|
517
|
+
const meta$3 = (label) => ({
|
|
518
|
+
label,
|
|
519
|
+
category: "length"
|
|
520
|
+
});
|
|
521
|
+
/** Characters of a string, or items of an array */
|
|
522
|
+
const lengthRules = {
|
|
523
|
+
length: defineRule({
|
|
524
|
+
kinds: kinds$2,
|
|
525
|
+
params: countParams,
|
|
526
|
+
meta: meta$3("Exact length"),
|
|
527
|
+
create: (p, m) => v.length(p.value, m)
|
|
528
|
+
}),
|
|
529
|
+
minLength: defineRule({
|
|
530
|
+
kinds: kinds$2,
|
|
531
|
+
params: countParams,
|
|
532
|
+
meta: meta$3("Minimum length"),
|
|
92
533
|
create: (p, m) => v.minLength(p.value, m)
|
|
93
534
|
}),
|
|
94
535
|
maxLength: defineRule({
|
|
95
|
-
kinds:
|
|
96
|
-
params:
|
|
536
|
+
kinds: kinds$2,
|
|
537
|
+
params: countParams,
|
|
538
|
+
meta: meta$3("Maximum length"),
|
|
97
539
|
create: (p, m) => v.maxLength(p.value, m)
|
|
98
540
|
}),
|
|
99
|
-
|
|
541
|
+
notLength: defineRule({
|
|
542
|
+
kinds: kinds$2,
|
|
543
|
+
params: countParams,
|
|
544
|
+
meta: meta$3("Not this length"),
|
|
545
|
+
create: (p, m) => v.notLength(p.value, m)
|
|
546
|
+
})
|
|
547
|
+
};
|
|
548
|
+
// src/rules/normalize-rules.ts
|
|
549
|
+
const stage = "normalize";
|
|
550
|
+
const meta$2 = (label, description) => ({
|
|
551
|
+
label,
|
|
552
|
+
description,
|
|
553
|
+
category: "normalize"
|
|
554
|
+
});
|
|
555
|
+
const limit = v.object({ value: v.number() });
|
|
556
|
+
/**
|
|
557
|
+
* Rules that change the value instead of checking it. They run before the required check, so
|
|
558
|
+
* `' '` with `trim` is empty. Their `message` is never shown.
|
|
559
|
+
*/
|
|
560
|
+
const normalizeRules = {
|
|
561
|
+
trim: defineRule({
|
|
100
562
|
kinds: ["string"],
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}),
|
|
105
|
-
create: (p, m) => v.regex(new RegExp(p.pattern, p.flags), m)
|
|
563
|
+
stage,
|
|
564
|
+
meta: meta$2("Trim"),
|
|
565
|
+
create: () => v.trim()
|
|
106
566
|
}),
|
|
107
|
-
|
|
567
|
+
trimStart: defineRule({
|
|
108
568
|
kinds: ["string"],
|
|
109
|
-
|
|
569
|
+
stage,
|
|
570
|
+
meta: meta$2("Trim start"),
|
|
571
|
+
create: () => v.trimStart()
|
|
110
572
|
}),
|
|
111
|
-
|
|
573
|
+
trimEnd: defineRule({
|
|
112
574
|
kinds: ["string"],
|
|
113
|
-
|
|
575
|
+
stage,
|
|
576
|
+
meta: meta$2("Trim end"),
|
|
577
|
+
create: () => v.trimEnd()
|
|
114
578
|
}),
|
|
115
|
-
|
|
116
|
-
kinds: ["
|
|
117
|
-
|
|
118
|
-
|
|
579
|
+
toLowerCase: defineRule({
|
|
580
|
+
kinds: ["string"],
|
|
581
|
+
stage,
|
|
582
|
+
meta: meta$2("Lowercase"),
|
|
583
|
+
create: () => v.toLowerCase()
|
|
119
584
|
}),
|
|
120
|
-
|
|
585
|
+
toUpperCase: defineRule({
|
|
586
|
+
kinds: ["string"],
|
|
587
|
+
stage,
|
|
588
|
+
meta: meta$2("Uppercase"),
|
|
589
|
+
create: () => v.toUpperCase()
|
|
590
|
+
}),
|
|
591
|
+
normalize: defineRule({
|
|
592
|
+
kinds: ["string"],
|
|
593
|
+
stage,
|
|
594
|
+
params: v.object({ form: v.optional(v.picklist([
|
|
595
|
+
"NFC",
|
|
596
|
+
"NFD",
|
|
597
|
+
"NFKC",
|
|
598
|
+
"NFKD"
|
|
599
|
+
])) }),
|
|
600
|
+
meta: meta$2("Unicode normalize", "Defaults to NFC."),
|
|
601
|
+
create: (p) => v.normalize(p.form)
|
|
602
|
+
}),
|
|
603
|
+
toMinValue: defineRule({
|
|
121
604
|
kinds: ["number"],
|
|
122
|
-
|
|
123
|
-
|
|
605
|
+
stage,
|
|
606
|
+
params: limit,
|
|
607
|
+
meta: meta$2("Raise to minimum", "Replaces a smaller number with the minimum."),
|
|
608
|
+
create: (p) => v.toMinValue(p.value)
|
|
124
609
|
}),
|
|
125
|
-
|
|
610
|
+
toMaxValue: defineRule({
|
|
126
611
|
kinds: ["number"],
|
|
612
|
+
stage,
|
|
613
|
+
params: limit,
|
|
614
|
+
meta: meta$2("Lower to maximum", "Replaces a larger number with the maximum."),
|
|
615
|
+
create: (p) => v.toMaxValue(p.value)
|
|
616
|
+
})
|
|
617
|
+
};
|
|
618
|
+
// src/rules/number-rules.ts
|
|
619
|
+
const kinds$1 = ["number"];
|
|
620
|
+
const meta$1 = (label) => ({
|
|
621
|
+
label,
|
|
622
|
+
category: "number"
|
|
623
|
+
});
|
|
624
|
+
const divisor = v.pipe(v.number(), v.finite(), v.notValue(0, "Can't be 0"));
|
|
625
|
+
/** Checks only numbers have. */
|
|
626
|
+
const numberRules = {
|
|
627
|
+
integer: defineRule({
|
|
628
|
+
kinds: kinds$1,
|
|
629
|
+
meta: meta$1("Whole number"),
|
|
127
630
|
create: (_, m) => v.integer(m)
|
|
631
|
+
}),
|
|
632
|
+
safeInteger: defineRule({
|
|
633
|
+
kinds: kinds$1,
|
|
634
|
+
meta: meta$1("Safe whole number"),
|
|
635
|
+
create: (_, m) => v.safeInteger(m)
|
|
636
|
+
}),
|
|
637
|
+
finite: defineRule({
|
|
638
|
+
kinds: kinds$1,
|
|
639
|
+
meta: meta$1("Finite number"),
|
|
640
|
+
create: (_, m) => v.finite(m)
|
|
641
|
+
}),
|
|
642
|
+
multipleOf: defineRule({
|
|
643
|
+
kinds: kinds$1,
|
|
644
|
+
params: v.object({ value: divisor }),
|
|
645
|
+
meta: meta$1("Multiple of"),
|
|
646
|
+
create: (p, m) => v.multipleOf(p.value, m)
|
|
647
|
+
})
|
|
648
|
+
};
|
|
649
|
+
// src/rules/text-unit-rules.ts
|
|
650
|
+
const kinds = ["string"];
|
|
651
|
+
const meta = (label, description) => ({
|
|
652
|
+
label,
|
|
653
|
+
description,
|
|
654
|
+
category: "text"
|
|
655
|
+
});
|
|
656
|
+
const isLocale = (input) => {
|
|
657
|
+
try {
|
|
658
|
+
return Intl.getCanonicalLocales(input).length === 1;
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
const wordsParams = v.object({
|
|
664
|
+
locale: v.optional(v.pipe(v.string(), v.check(isLocale, "Use a language tag, such as en or sr-Latn"))),
|
|
665
|
+
value: count
|
|
666
|
+
});
|
|
667
|
+
const graphemesInfo = "Counts characters as people see them, so an emoji is one.";
|
|
668
|
+
const bytesInfo = "Counts UTF-8 bytes, e.g. to fit a database column,";
|
|
669
|
+
const textUnitRules = {
|
|
670
|
+
graphemes: defineRule({
|
|
671
|
+
kinds,
|
|
672
|
+
params: countParams,
|
|
673
|
+
meta: meta("Exact characters", graphemesInfo),
|
|
674
|
+
create: (p, m) => v.graphemes(p.value, m)
|
|
675
|
+
}),
|
|
676
|
+
minGraphemes: defineRule({
|
|
677
|
+
kinds,
|
|
678
|
+
params: countParams,
|
|
679
|
+
meta: meta("Minimum characters", graphemesInfo),
|
|
680
|
+
create: (p, m) => v.minGraphemes(p.value, m)
|
|
681
|
+
}),
|
|
682
|
+
maxGraphemes: defineRule({
|
|
683
|
+
kinds,
|
|
684
|
+
params: countParams,
|
|
685
|
+
meta: meta("Maximum characters", graphemesInfo),
|
|
686
|
+
create: (p, m) => v.maxGraphemes(p.value, m)
|
|
687
|
+
}),
|
|
688
|
+
notGraphemes: defineRule({
|
|
689
|
+
kinds,
|
|
690
|
+
params: countParams,
|
|
691
|
+
meta: meta("Not this many characters", graphemesInfo),
|
|
692
|
+
create: (p, m) => v.notGraphemes(p.value, m)
|
|
693
|
+
}),
|
|
694
|
+
words: defineRule({
|
|
695
|
+
kinds,
|
|
696
|
+
params: wordsParams,
|
|
697
|
+
meta: meta("Exact words"),
|
|
698
|
+
create: (p, m) => v.words(p.locale, p.value, m)
|
|
699
|
+
}),
|
|
700
|
+
minWords: defineRule({
|
|
701
|
+
kinds,
|
|
702
|
+
params: wordsParams,
|
|
703
|
+
meta: meta("Minimum words"),
|
|
704
|
+
create: (p, m) => v.minWords(p.locale, p.value, m)
|
|
705
|
+
}),
|
|
706
|
+
maxWords: defineRule({
|
|
707
|
+
kinds,
|
|
708
|
+
params: wordsParams,
|
|
709
|
+
meta: meta("Maximum words"),
|
|
710
|
+
create: (p, m) => v.maxWords(p.locale, p.value, m)
|
|
711
|
+
}),
|
|
712
|
+
notWords: defineRule({
|
|
713
|
+
kinds,
|
|
714
|
+
params: wordsParams,
|
|
715
|
+
meta: meta("Not this many words"),
|
|
716
|
+
create: (p, m) => v.notWords(p.locale, p.value, m)
|
|
717
|
+
}),
|
|
718
|
+
bytes: defineRule({
|
|
719
|
+
kinds,
|
|
720
|
+
params: countParams,
|
|
721
|
+
meta: meta("Exact bytes", bytesInfo),
|
|
722
|
+
create: (p, m) => v.bytes(p.value, m)
|
|
723
|
+
}),
|
|
724
|
+
minBytes: defineRule({
|
|
725
|
+
kinds,
|
|
726
|
+
params: countParams,
|
|
727
|
+
meta: meta("Minimum bytes", bytesInfo),
|
|
728
|
+
create: (p, m) => v.minBytes(p.value, m)
|
|
729
|
+
}),
|
|
730
|
+
maxBytes: defineRule({
|
|
731
|
+
kinds,
|
|
732
|
+
params: countParams,
|
|
733
|
+
meta: meta("Maximum bytes", bytesInfo),
|
|
734
|
+
create: (p, m) => v.maxBytes(p.value, m)
|
|
735
|
+
}),
|
|
736
|
+
notBytes: defineRule({
|
|
737
|
+
kinds,
|
|
738
|
+
params: countParams,
|
|
739
|
+
meta: meta("Not this many bytes", bytesInfo),
|
|
740
|
+
create: (p, m) => v.notBytes(p.value, m)
|
|
128
741
|
})
|
|
129
742
|
};
|
|
743
|
+
// src/rules/default-rules.ts
|
|
744
|
+
/** Every built-in rule. Register single families with `includeDefaultRules: false`. */
|
|
745
|
+
const defaultRules = {
|
|
746
|
+
...lengthRules,
|
|
747
|
+
...textUnitRules,
|
|
748
|
+
...contentRules,
|
|
749
|
+
...formatRules,
|
|
750
|
+
...dateRules,
|
|
751
|
+
...comparisonRules,
|
|
752
|
+
...numberRules,
|
|
753
|
+
...fileRules,
|
|
754
|
+
...normalizeRules
|
|
755
|
+
};
|
|
130
756
|
// src/builder/registry.ts
|
|
131
757
|
/** Consumer entries replace defaults with the same key. */
|
|
132
758
|
function mergeRegistry(custom) {
|
|
759
|
+
const rules = custom.includeDefaultRules === false ? {} : defaultRules;
|
|
133
760
|
return {
|
|
134
761
|
kinds: Object.freeze({
|
|
135
762
|
...defaultKinds,
|
|
@@ -140,7 +767,7 @@ function mergeRegistry(custom) {
|
|
|
140
767
|
...custom.fieldTypes
|
|
141
768
|
}),
|
|
142
769
|
rules: Object.freeze({
|
|
143
|
-
...
|
|
770
|
+
...rules,
|
|
144
771
|
...custom.rules
|
|
145
772
|
})
|
|
146
773
|
};
|
|
@@ -165,6 +792,22 @@ var DefinitionError = class extends Error {
|
|
|
165
792
|
this.issues = issues;
|
|
166
793
|
}
|
|
167
794
|
};
|
|
795
|
+
// src/rules/rule-params.ts
|
|
796
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
797
|
+
function paramsFor(def, kind) {
|
|
798
|
+
if (typeof def.params !== "function") return def.params;
|
|
799
|
+
let byKind = cache.get(def);
|
|
800
|
+
if (!byKind) {
|
|
801
|
+
byKind = /* @__PURE__ */ new Map();
|
|
802
|
+
cache.set(def, byKind);
|
|
803
|
+
}
|
|
804
|
+
let params = byKind.get(kind);
|
|
805
|
+
if (!params) {
|
|
806
|
+
params = def.params(kind);
|
|
807
|
+
byKind.set(kind, params);
|
|
808
|
+
}
|
|
809
|
+
return params;
|
|
810
|
+
}
|
|
168
811
|
// src/shared/reserved-keys.ts
|
|
169
812
|
/**
|
|
170
813
|
* Field keys this package owns. A field type's props can't use them.
|
|
@@ -266,26 +909,53 @@ function checkField(raw, path, registry, names, issues) {
|
|
|
266
909
|
}
|
|
267
910
|
return;
|
|
268
911
|
}
|
|
269
|
-
const
|
|
912
|
+
const withProps = props && {
|
|
913
|
+
...field,
|
|
914
|
+
...props
|
|
915
|
+
};
|
|
916
|
+
const kind = resolveKind(type.kind, withProps);
|
|
917
|
+
if (kind === void 0) return void 0;
|
|
918
|
+
const kinds = {
|
|
919
|
+
kind,
|
|
920
|
+
itemKind: kind === "array" ? resolveKind(type.itemKind, withProps) : void 0
|
|
921
|
+
};
|
|
922
|
+
const fieldRules = (shape.output.rules ?? []).flatMap((rule, j) => checkRule(rule, kinds, [
|
|
270
923
|
...path,
|
|
271
924
|
"rules",
|
|
272
925
|
j
|
|
273
926
|
], registry, issues) ?? []);
|
|
274
|
-
if (issues.length > issueCount || !field.name) return void 0;
|
|
927
|
+
if (issues.length > issueCount || !field.name || !withProps) return void 0;
|
|
275
928
|
const overridden = new Set(fieldRules.map((rule) => rule.name));
|
|
276
|
-
const defaultRules = (type
|
|
929
|
+
const defaultRules = checkDefaultRules(field.type, type, kinds, registry).filter((rule) => !overridden.has(rule.name));
|
|
277
930
|
return {
|
|
278
931
|
name: field.name,
|
|
279
|
-
field:
|
|
280
|
-
...field,
|
|
281
|
-
...props
|
|
282
|
-
},
|
|
932
|
+
field: withProps,
|
|
283
933
|
path,
|
|
284
934
|
type,
|
|
285
|
-
kind:
|
|
935
|
+
kind: kindDef(field.type, kind, registry),
|
|
286
936
|
rules: [...defaultRules, ...fieldRules]
|
|
287
937
|
};
|
|
288
938
|
}
|
|
939
|
+
function resolveKind(kind, field) {
|
|
940
|
+
if (typeof kind !== "function") return kind;
|
|
941
|
+
return field && kind(field);
|
|
942
|
+
}
|
|
943
|
+
/** A kind that doesn't fit is a mistake in the field type, not in the definition. */
|
|
944
|
+
function kindDef(typeName, kind, registry) {
|
|
945
|
+
const at = `Field type "${typeName}"`;
|
|
946
|
+
if (kind === "none") throw new TypeError(`${at}: kind() can't return 'none'`);
|
|
947
|
+
const def = lookup(registry.kinds, kind);
|
|
948
|
+
if (!def) throw new TypeError(`${at}: kind "${kind}" is not registered`);
|
|
949
|
+
return def;
|
|
950
|
+
}
|
|
951
|
+
/** Throws for a default rule that doesn't fit, because that is a mistake in the type. */
|
|
952
|
+
function checkDefaultRules(typeName, type, kinds, registry) {
|
|
953
|
+
const issues = [];
|
|
954
|
+
const rules = (type.defaultRules ?? []).flatMap((rule, j) => checkRule(rule, kinds, ["defaultRules", j], registry, issues) ?? []);
|
|
955
|
+
const [first] = issues;
|
|
956
|
+
if (first) throw new TypeError(`Field type "${typeName}": ${formatPath(first.path)}: ${first.message}`);
|
|
957
|
+
return rules;
|
|
958
|
+
}
|
|
289
959
|
function checkProps(field, type, path, issues) {
|
|
290
960
|
const propKeys = Object.keys(type.props?.entries ?? {});
|
|
291
961
|
const input = {};
|
|
@@ -298,9 +968,8 @@ function checkProps(field, type, path, issues) {
|
|
|
298
968
|
const parsed = v.safeParse(type.props, input);
|
|
299
969
|
if (parsed.success) return parsed.output;
|
|
300
970
|
issues.push(...toIssues("invalid_props", path, parsed.issues));
|
|
301
|
-
return {};
|
|
302
971
|
}
|
|
303
|
-
function checkRule(raw, kind, path, registry, issues) {
|
|
972
|
+
function checkRule(raw, { kind, itemKind }, path, registry, issues) {
|
|
304
973
|
const shape = v.safeParse(ruleShape, raw);
|
|
305
974
|
if (!shape.success) {
|
|
306
975
|
issues.push(...toIssues("invalid_shape", path, shape.issues));
|
|
@@ -312,41 +981,44 @@ function checkRule(raw, kind, path, registry, issues) {
|
|
|
312
981
|
issues.push(issue("unknown_rule", [...path, "rule"], `Unknown rule "${rule.rule}"`));
|
|
313
982
|
return;
|
|
314
983
|
}
|
|
315
|
-
const
|
|
316
|
-
if (!def.kinds.includes(kind)) {
|
|
317
|
-
const
|
|
984
|
+
const each = !def.kinds.includes(kind) && !!itemKind && def.kinds.includes(itemKind);
|
|
985
|
+
if (!def.kinds.includes(kind) && !each) {
|
|
986
|
+
const items = itemKind ? ` or their ${itemKind} items` : "";
|
|
987
|
+
const message = `Rule "${rule.rule}" can't be used on ${kind} fields${items}`;
|
|
318
988
|
issues.push(issue("kind_mismatch", [...path, "rule"], message));
|
|
989
|
+
return;
|
|
319
990
|
}
|
|
320
|
-
const
|
|
991
|
+
const issueCount = issues.length;
|
|
992
|
+
const params = paramsFor(def, each ? itemKind : kind);
|
|
993
|
+
const paramKeys = Object.keys(params?.entries ?? {});
|
|
321
994
|
const input = {};
|
|
322
995
|
for (const key of Object.keys(rule)) {
|
|
323
996
|
if (isReservedRuleKey(key)) continue;
|
|
324
997
|
if (paramKeys.includes(key)) input[key] = rule[key];
|
|
325
998
|
else issues.push(issue("unknown_param", [...path, key], `Rule "${rule.rule}" has no param "${key}"`));
|
|
326
999
|
}
|
|
327
|
-
const parsed = v.safeParse(
|
|
1000
|
+
const parsed = v.safeParse(params ?? noParams, input);
|
|
328
1001
|
if (!parsed.success) issues.push(...toIssues("invalid_params", path, parsed.issues));
|
|
329
1002
|
if (!parsed.success || issues.length > issueCount) return void 0;
|
|
330
1003
|
return {
|
|
331
1004
|
name: rule.rule,
|
|
332
1005
|
def,
|
|
333
1006
|
params: parsed.output,
|
|
334
|
-
message: rule.message
|
|
1007
|
+
message: rule.message,
|
|
1008
|
+
each
|
|
335
1009
|
};
|
|
336
1010
|
}
|
|
337
1011
|
function checkRegistry(registry) {
|
|
338
|
-
for (const [name, rule] of Object.entries(registry.rules)) for (const key of Object.keys(rule
|
|
1012
|
+
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}"`);
|
|
339
1013
|
for (const [name, type] of Object.entries(registry.fieldTypes)) {
|
|
340
1014
|
const at = `Field type "${name}"`;
|
|
341
|
-
if (!lookup(registry.kinds,
|
|
1015
|
+
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`);
|
|
342
1016
|
if (type.kind !== "none" && !type.schema) throw new TypeError(`${at}: schema() is required unless the kind is 'none'`);
|
|
343
1017
|
for (const key of Object.keys(type.props?.entries ?? {})) if (isReservedFieldKey(key)) throw new TypeError(`${at}: props can't use the reserved key "${key}"`);
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
});
|
|
348
|
-
const [first] = issues;
|
|
349
|
-
if (first) throw new TypeError(`${at}: ${formatPath(first.path)}: ${first.message}`);
|
|
1018
|
+
if (typeof type.kind === "string" && typeof type.itemKind !== "function") checkDefaultRules(name, type, {
|
|
1019
|
+
kind: type.kind,
|
|
1020
|
+
itemKind: type.kind === "array" ? type.itemKind : void 0
|
|
1021
|
+
}, registry);
|
|
350
1022
|
}
|
|
351
1023
|
}
|
|
352
1024
|
function issue(code, path, message) {
|
|
@@ -388,16 +1060,21 @@ function createSchemaBuilder(options = {}) {
|
|
|
388
1060
|
for (const field of fields) entries[field.name] = buildFieldSchema(field, messages);
|
|
389
1061
|
return v.object(entries);
|
|
390
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* The registered rules that can be used on a field of `kind`, e.g. for a rule picker.
|
|
1065
|
+
*/
|
|
1066
|
+
function rulesForKind(kind) {
|
|
1067
|
+
return Object.entries(registry.rules).filter(([, def]) => def.kinds.includes(kind));
|
|
1068
|
+
}
|
|
391
1069
|
return {
|
|
392
1070
|
build,
|
|
393
1071
|
validate,
|
|
1072
|
+
rulesForKind,
|
|
394
1073
|
kinds: registry.kinds,
|
|
395
1074
|
fieldTypes: registry.fieldTypes,
|
|
396
1075
|
rules: registry.rules
|
|
397
1076
|
};
|
|
398
1077
|
}
|
|
399
|
-
|
|
400
|
-
const defineFieldType = (def) => def;
|
|
401
|
-
export { DefinitionError, RESERVED_FIELD_KEYS, RESERVED_RULE_KEYS, createSchemaBuilder, defaultFieldTypes, defaultKinds, defaultRules, defineFieldType, defineRule, formatPath };
|
|
1078
|
+
export { DefinitionError, RESERVED_FIELD_KEYS, RESERVED_RULE_KEYS, comparisonRules, contentRules, createSchemaBuilder, dateRules, defaultFieldTypes, defaultKinds, defaultRules, defineFieldType, defineRule, fileRules, formatPath, formatRules, lengthRules, normalizeRules, numberRules, textUnitRules };
|
|
402
1079
|
|
|
403
1080
|
//# sourceMappingURL=index.js.map
|