@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 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 JsonFormSchema } from '@whatsaaaa/form-builder-schema'
16
+ import { createSchemaBuilder, type JsonForm } from '@whatsaaaa/form-builder-schema'
15
17
 
16
- const json: JsonFormSchema = {
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 inputs of optional `number` fields (`''` or `undefined`) produce `undefined`.
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
- Custom field types and rules can be passed to `createSchemaBuilder({ fieldTypes, rules })`. Use `defineRule` to get typed rule params.
142
+ `resolveMessage` returning `undefined` keeps the text as written.
36
143
 
37
144
  ## License
38
145