@whatsaaaa/form-builder-schema 0.1.0 → 0.3.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 +113 -6
- package/dist/index.d.ts +519 -29
- package/dist/index.js +840 -80
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
package/README.md
CHANGED
|
@@ -8,15 +8,17 @@ Build [Valibot](https://valibot.dev) schemas from JSON form definitions, ready t
|
|
|
8
8
|
pnpm add @whatsaaaa/form-builder-schema valibot
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Valibot 1.3 or later is required.
|
|
12
|
+
|
|
11
13
|
## Usage
|
|
12
14
|
|
|
13
15
|
```ts
|
|
14
|
-
import { createSchemaBuilder, type
|
|
16
|
+
import { createSchemaBuilder, type JsonForm } from '@whatsaaaa/form-builder-schema'
|
|
15
17
|
|
|
16
|
-
const json:
|
|
18
|
+
const json: JsonForm = {
|
|
17
19
|
version: '1',
|
|
18
20
|
fields: [
|
|
19
|
-
{ name: 'email', type: 'email', required: { message: 'Email is required' } },
|
|
21
|
+
{ name: 'email', type: 'email', label: 'Email', required: { message: 'Email is required' } },
|
|
20
22
|
{
|
|
21
23
|
name: 'password',
|
|
22
24
|
type: 'password',
|
|
@@ -26,13 +28,118 @@ const json: JsonFormSchema = {
|
|
|
26
28
|
],
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
const build = createSchemaBuilder()
|
|
31
|
+
const { build, validate } = createSchemaBuilder()
|
|
30
32
|
const schema = build(json) // v.ObjectSchema, ready for Formisch or v.parse
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
Built-in field types are `text`, `email`, `password` and `number`. Empty
|
|
35
|
+
Built-in field types are `text`, `email`, `password` and `number`. Empty optional fields output `undefined`. Rules never run on empty values, and a required empty field reports only the required message.
|
|
36
|
+
|
|
37
|
+
## Rules
|
|
38
|
+
|
|
39
|
+
Every Valibot action whose arguments fit in JSON is a rule with the same name. Params go next to `rule`, and a rule without a `message` uses Valibot's message, so [`@valibot/i18n`](https://valibot.dev/guides/internationalization/) works.
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{ "rule": "minLength", "value": 3, "message": "At least 3 characters" }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Family | Rules | Kinds | Params |
|
|
46
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ | -------------------------------------- |
|
|
47
|
+
| `lengthRules` | `length` `minLength` `maxLength` `notLength` | string, array | `value` |
|
|
48
|
+
| `textUnitRules` | `graphemes` `minGraphemes` `maxGraphemes` `notGraphemes`, `words` `minWords` `maxWords` `notWords`, `bytes` `minBytes` `maxBytes` `notBytes` | string | `value`; words also `locale` |
|
|
49
|
+
| `contentRules` | `regex` `startsWith` `endsWith` `includes` `excludes` | string; `includes` and `excludes` also array | `pattern` and `flags`, or `value` |
|
|
50
|
+
| `formatRules` | `email` `rfcEmail` `url` `domain` `slug` `uuid` `ulid` `cuid2` `nanoid` `ip` `ipv4` `ipv6` `mac` `mac48` `mac64` `hexColor` `hexadecimal` `octal` `decimal` `digits` `base64` `bic` `creditCard` `imei` `isbn` `isrc` `emoji` `hash` | string | none; `hash` takes `types` |
|
|
51
|
+
| `dateRules` | `isoDate` `isoDateTime` `isoTime` `isoTimeSecond` `isoTimestamp` `isoWeek` | string, date | none |
|
|
52
|
+
| `comparisonRules` | `minValue` `maxValue` `gtValue` `ltValue` `value` `notValue` `values` `notValues` | number, date; `value` and `notValue` also string and boolean | `value` or `values`, typed by the kind |
|
|
53
|
+
| `numberRules` | `integer` `safeInteger` `finite` `multipleOf` | number | `multipleOf` takes `value` |
|
|
54
|
+
| `fileRules` | `minSize` `maxSize` `size` `notSize` `mimeType` | file | `value` in bytes, or `types` |
|
|
55
|
+
| `normalizeRules` | `trim` `trimStart` `trimEnd` `toLowerCase` `toUpperCase` `normalize`, `toMinValue` `toMaxValue` | string; `toMinValue` and `toMaxValue` on number | none; `form`, or `value` |
|
|
56
|
+
|
|
57
|
+
- **Normalize rules** change the value instead of checking it, and run before the required check. With `trim`, `' '` counts as empty.
|
|
58
|
+
- **`regex`** takes `pattern` (without slashes) and optional `flags`. A pattern `RegExp` rejects is an `invalid_params` issue. `g` and `y` are accepted but ignored, because they make a regex skip ahead on the next value. If end users write patterns, beware of [ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS): a pattern like `(a+)+$` can freeze the page.
|
|
59
|
+
- **Comparing dates.** On a `date` field, `value` must be an ISO date, date-time or time string. Valibot compares them as text, so use the same format as the field: `'2026-01-01'` for a date, `'2026-01-01T09:00'` for a date-time.
|
|
60
|
+
- **`words`** splits text by language. Without `locale`, the runtime's default locale is used.
|
|
61
|
+
- Function-based actions (`check`, `transform`, …) can't be written in JSON. Register them as your own rules with `defineRule`.
|
|
62
|
+
|
|
63
|
+
### Registering only some rules
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { createSchemaBuilder, formatRules, lengthRules } from '@whatsaaaa/form-builder-schema'
|
|
67
|
+
|
|
68
|
+
const { build } = createSchemaBuilder({
|
|
69
|
+
includeDefaultRules: false,
|
|
70
|
+
rules: { ...formatRules, ...lengthRules },
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The built-in `email` type uses the `email` rule, so keep `formatRules` (or replace the `email` type), or `createSchemaBuilder` throws a `TypeError`.
|
|
75
|
+
|
|
76
|
+
### Listing rules
|
|
77
|
+
|
|
78
|
+
Every built-in rule has `meta` with a `label`, a `category` and sometimes a `description`, for a rule picker. `rulesForKind(kind)` returns the `[name, def]` pairs that fit a kind:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const { rulesForKind } = createSchemaBuilder()
|
|
82
|
+
rulesForKind('file').map(([name, def]) => def.meta?.label) // ['Minimum size', …]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Checking definitions
|
|
86
|
+
|
|
87
|
+
`validate(json)` never throws. It returns every problem in the definition, in document order:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const result = validate(jsonFromDatabase)
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
for (const issue of result.issues) console.log(formatPath(issue.path), issue.code, issue.message)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`build(json)` runs the same checks and throws one `DefinitionError` with all of them. Issue codes are `invalid_shape`, `unsupported_version`, `unknown_key`, `unknown_type`, `missing_name`, `duplicate_name`, `invalid_props`, `unknown_rule`, `unknown_param`, `invalid_params` and `kind_mismatch`.
|
|
97
|
+
|
|
98
|
+
Keys on a field must be reserved keys (`RESERVED_FIELD_KEYS`) or props of its type. Put renderer settings under `ui`, which is never checked.
|
|
99
|
+
|
|
100
|
+
## Extending
|
|
101
|
+
|
|
102
|
+
`createSchemaBuilder({ kinds, fieldTypes, rules })` merges your entries over the defaults by key. The merged registries are returned as `kinds`, `fieldTypes` and `rules`.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const select = defineFieldType({
|
|
106
|
+
kind: 'string',
|
|
107
|
+
props: v.object({ options: v.array(v.string()) }),
|
|
108
|
+
schema: (field) => v.picklist(field.options), // field.options is typed
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const even = defineRule({
|
|
112
|
+
kinds: ['number'],
|
|
113
|
+
create: (_, message) => v.check((n: number) => n % 2 === 0, message),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const { build } = createSchemaBuilder({ fieldTypes: { select }, rules: { even } })
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- A **kind** says what "empty" means for a value (`isEmpty`) and adds an optional extra `required` check. Built-in kinds are `string`, `number`, `boolean`, `date`, `array`, `file`, `object` and `none`. A type with kind `none` has no value, like a heading.
|
|
120
|
+
- A **rule** with `stage: 'normalize'` runs before the required check. Its `params` schema lists the only keys the rule accepts. `params` can also be a function of the field's kind, for params whose type depends on it:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const atLeast = defineRule({
|
|
124
|
+
kinds: ['number', 'date'],
|
|
125
|
+
params: (kind) => v.object({ value: kind === 'date' ? v.string() : v.number() }),
|
|
126
|
+
meta: { label: 'At least', category: 'comparison' },
|
|
127
|
+
create: (params, message) => v.minValue(params.value, message),
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- Mistakes in the registry, such as an unknown kind or a reserved props key, throw a `TypeError` from `createSchemaBuilder`.
|
|
132
|
+
|
|
133
|
+
## Messages
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
createSchemaBuilder({
|
|
137
|
+
messages: { required: 'Obavezno' },
|
|
138
|
+
resolveMessage: (key) => t(key), // every message from the definition, and messages.required
|
|
139
|
+
})
|
|
140
|
+
```
|
|
34
141
|
|
|
35
|
-
|
|
142
|
+
`resolveMessage` returning `undefined` keeps the text as written.
|
|
36
143
|
|
|
37
144
|
## License
|
|
38
145
|
|