@whatsaaaa/form-builder-schema 0.5.0 → 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 +57 -3
- package/dist/index.d.ts +55 -2
- package/dist/index.js +398 -119
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ const { build } = createSchemaBuilder({
|
|
|
129
129
|
})
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
The built-in `email` and `url` types use the `email` and `url` rules, so keep `formatRules` (or replace those types), or `createSchemaBuilder` throws a `TypeError`.
|
|
132
|
+
The built-in `email` and `url` types use the `email` and `url` rules, so keep `formatRules` (or replace those types), or `createSchemaBuilder` throws a `TypeError`. `includeDefaultRules: false` leaves out the built-in form rules too; add them back with `formRules: defaultFormRules`.
|
|
133
133
|
|
|
134
134
|
### Listing rules
|
|
135
135
|
|
|
@@ -140,6 +140,43 @@ const { rulesForKind } = createSchemaBuilder()
|
|
|
140
140
|
rulesForKind('file').map(([name, def]) => def.meta?.label) // ['Minimum size', …]
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
## Form rules
|
|
144
|
+
|
|
145
|
+
A form rule checks a field against other fields, such as a repeated password or a date range. Form rules go in `formRules`, next to `fields`, and each one names the `field` its error shows on:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"version": "1",
|
|
150
|
+
"fields": [
|
|
151
|
+
{ "name": "password", "type": "password", "required": true },
|
|
152
|
+
{ "name": "password2", "type": "password", "required": true },
|
|
153
|
+
{ "name": "start", "type": "date" },
|
|
154
|
+
{ "name": "end", "type": "date" }
|
|
155
|
+
],
|
|
156
|
+
"formRules": [
|
|
157
|
+
{ "rule": "sameAs", "field": "password2", "other": "password", "message": "Passwords differ" },
|
|
158
|
+
{
|
|
159
|
+
"rule": "compare",
|
|
160
|
+
"field": "end",
|
|
161
|
+
"op": "gt",
|
|
162
|
+
"other": "start",
|
|
163
|
+
"message": "After the start"
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
| Rule | Params | Kinds | Passes when |
|
|
170
|
+
| --------------- | ------------------------------------------ | ----------------------------- | ----------------- |
|
|
171
|
+
| `sameAs` | `other` | string, number, boolean, date | `field === other` |
|
|
172
|
+
| `differentFrom` | `other` | string, number, boolean, date | `field !== other` |
|
|
173
|
+
| `compare` | `op` (`lt`, `lte`, `gt` or `gte`), `other` | number, date | `field op other` |
|
|
174
|
+
|
|
175
|
+
- **Paths** name fields by their value: `'password2'`, `'address.zip'` for a field in a group, and `'lines.*.qty'` for a field in the items of an array. A rule on a path with `*` runs once per item, and its error shows on that item's field. A path it reads with `*` means the same item, so `{ "field": "lines.*.qty", "op": "lte", "other": "lines.*.stock" }` compares each line with itself.
|
|
176
|
+
- **A form rule runs last**, after every field's own rules, and only when the fields it reads have no issues. So a password that is too short never also shows "Passwords differ". It never runs on an empty value either: make the fields required when they must be filled.
|
|
177
|
+
- **`message` is required.** It goes through `resolveMessage` like every other message, with the rule's field in `ctx.field`.
|
|
178
|
+
- The fields a rule reads must have the same kind: `compare` on a number and a date is `kind_mismatch`. A date with `output: 'date'` is compared by its time, so compare it with another date that has `output: 'date'`.
|
|
179
|
+
|
|
143
180
|
## Checking definitions
|
|
144
181
|
|
|
145
182
|
`validate(json)` never throws. It returns every problem in the definition, in document order:
|
|
@@ -151,16 +188,17 @@ if (!result.success) {
|
|
|
151
188
|
}
|
|
152
189
|
```
|
|
153
190
|
|
|
154
|
-
`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`, `invalid_name`, `duplicate_name`, `invalid_props`, `invalid_default`, `unknown_rule`, `unknown_param`, `invalid_params` and `
|
|
191
|
+
`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`, `invalid_name`, `duplicate_name`, `invalid_props`, `invalid_default`, `unknown_rule`, `unknown_param`, `invalid_params`, `kind_mismatch` and `unknown_field`. An issue in a nested field has its full path, such as `['fields', 2, 'fields', 0, 'rules', 1]`.
|
|
155
192
|
|
|
156
193
|
- **Names** are one key of the value, so a name with `.`, `[` or `]` is `invalid_name`.
|
|
157
194
|
- **Defaults** must pass the field's own schema, rules included, or they are `invalid_default`. An empty default, like `''`, counts as none.
|
|
195
|
+
- **Form rules** report on `['formRules', 0]` and below. A `field` path that names no field with a value is `unknown_field` on `['formRules', 0, 'field']`, and a path the rule reads gets it on `['formRules', 0]`. When a field the rule reads has issues of its own, the rule adds none.
|
|
158
196
|
|
|
159
197
|
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.
|
|
160
198
|
|
|
161
199
|
## Extending
|
|
162
200
|
|
|
163
|
-
`createSchemaBuilder({ kinds, fieldTypes, rules })` merges your entries over the defaults by key. The merged registries are returned as `kinds`, `fieldTypes` and `
|
|
201
|
+
`createSchemaBuilder({ kinds, fieldTypes, rules, formRules })` merges your entries over the defaults by key. The merged registries are returned as `kinds`, `fieldTypes`, `rules` and `formRules`.
|
|
164
202
|
|
|
165
203
|
```ts
|
|
166
204
|
const tags = defineFieldType({
|
|
@@ -198,6 +236,22 @@ const { build } = createSchemaBuilder({ fieldTypes: { tags }, rules: { even } })
|
|
|
198
236
|
const schema = myType.schema?.(field, createBuildContext({ path: ['fields', 0] }))
|
|
199
237
|
```
|
|
200
238
|
|
|
239
|
+
- A **form rule** lists the paths it reads besides `field`, and checks their values with a plain function. The builder checks that the fields exist and have one of `kinds`, and runs `check` only when none of them has an issue or is empty:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const sameZip = defineFormRule({
|
|
243
|
+
params: v.object({ others: v.array(v.string()) }),
|
|
244
|
+
reads: (params) => params.others,
|
|
245
|
+
kinds: ['string'],
|
|
246
|
+
check: (values) => new Set(values).size === 1, // the value of `field`, then the others
|
|
247
|
+
meta: { label: 'Same ZIP', category: 'comparison' },
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const { build } = createSchemaBuilder({ formRules: { sameZip } })
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`sameKind: true` requires every field it reads to have the same kind. `rule`, `field` and `message` are reserved (`RESERVED_FORM_RULE_KEYS`), so params can't use them.
|
|
254
|
+
|
|
201
255
|
- Mistakes in the registry, such as an unknown kind or a reserved props key, throw a `TypeError` from `createSchemaBuilder`.
|
|
202
256
|
|
|
203
257
|
## Initial input and walking fields
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ type ValueKind = 'string' | 'number' | 'boolean' | 'date' | 'array' | 'file' | '
|
|
|
12
12
|
interface JsonForm {
|
|
13
13
|
version: '1';
|
|
14
14
|
fields: readonly JsonField[];
|
|
15
|
+
/** Rules that check a field against other fields. They run after every field's own rules. */
|
|
16
|
+
formRules?: readonly JsonFormRule[];
|
|
15
17
|
}
|
|
16
18
|
interface JsonField {
|
|
17
19
|
name?: string;
|
|
@@ -34,10 +36,17 @@ interface JsonRule {
|
|
|
34
36
|
message?: string;
|
|
35
37
|
[param: string]: unknown;
|
|
36
38
|
}
|
|
39
|
+
interface JsonFormRule {
|
|
40
|
+
rule: string;
|
|
41
|
+
/** Where the error shows, as a dot path: `'password2'`, `'address.zip'` or `'lines.*.qty'`. */
|
|
42
|
+
field: string;
|
|
43
|
+
message: string;
|
|
44
|
+
[param: string]: unknown;
|
|
45
|
+
}
|
|
37
46
|
/** Where something is in a definition, e.g. `['fields', 0, 'rules', 1]`. */
|
|
38
47
|
type DefinitionPath = readonly (string | number)[];
|
|
39
48
|
/** Public API: adding a code is a minor change, renaming one is breaking. */
|
|
40
|
-
type DefinitionIssueCode = 'invalid_shape' | 'unsupported_version' | 'unknown_key' | 'unknown_type' | 'missing_name' | 'invalid_name' | 'duplicate_name' | 'invalid_props' | 'invalid_default' | 'unknown_rule' | 'unknown_param' | 'invalid_params' | 'kind_mismatch';
|
|
49
|
+
type DefinitionIssueCode = 'invalid_shape' | 'unsupported_version' | 'unknown_key' | 'unknown_type' | 'missing_name' | 'invalid_name' | 'duplicate_name' | 'invalid_props' | 'invalid_default' | 'unknown_rule' | 'unknown_param' | 'invalid_params' | 'kind_mismatch' | 'unknown_field';
|
|
41
50
|
interface DefinitionIssue {
|
|
42
51
|
code: DefinitionIssueCode;
|
|
43
52
|
path: DefinitionPath;
|
|
@@ -150,6 +159,29 @@ interface RuleDef<TParams extends AnyObjectSchema = AnyObjectSchema> {
|
|
|
150
159
|
/** Method syntax keeps `create` bivariant, so specific rules fit in a `Record<string, RuleDef>`. */
|
|
151
160
|
create(params: v.InferOutput<TParams>, message?: string): AnyAction;
|
|
152
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* A rule that checks a field against other fields, e.g. that two passwords match. It goes in the
|
|
164
|
+
* definition's `formRules`, and its error shows on the field named by `field`.
|
|
165
|
+
*/
|
|
166
|
+
interface FormRuleDef<TParams extends AnyObjectSchema = AnyObjectSchema> {
|
|
167
|
+
/** Its params, next to `rule`, `field` and `message` in the definition. Unknown ones fail. */
|
|
168
|
+
params?: TParams;
|
|
169
|
+
/**
|
|
170
|
+
* The paths the rule reads besides `field`, e.g. `[params.other]`, so the builder can check
|
|
171
|
+
* that those fields exist. Method syntax keeps it bivariant, like `check()`.
|
|
172
|
+
*/
|
|
173
|
+
reads(params: v.InferOutput<TParams>): readonly string[];
|
|
174
|
+
/** The kinds the fields it reads may have. Any kind when omitted. */
|
|
175
|
+
kinds?: readonly ValueKind[];
|
|
176
|
+
/** True when every field it reads must have the same kind, e.g. to compare them. */
|
|
177
|
+
sameKind?: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* True when the values pass. `values` holds the value of `field`, then the values of the paths
|
|
180
|
+
* from `reads()`, in that order. It only runs when all of them are valid and none is empty.
|
|
181
|
+
*/
|
|
182
|
+
check(values: readonly unknown[], params: v.InferOutput<TParams>): boolean;
|
|
183
|
+
meta?: RuleMeta;
|
|
184
|
+
}
|
|
153
185
|
interface MessageContext {
|
|
154
186
|
field: JsonField;
|
|
155
187
|
rule?: string;
|
|
@@ -161,6 +193,8 @@ interface SchemaBuilderOptions {
|
|
|
161
193
|
kinds?: Record<string, KindDef>;
|
|
162
194
|
fieldTypes?: Record<string, FieldTypeDef>;
|
|
163
195
|
rules?: Record<string, RuleDef>;
|
|
196
|
+
formRules?: Record<string, FormRuleDef>;
|
|
197
|
+
/** With false, only your own rules and form rules are registered. Defaults to true. */
|
|
164
198
|
includeDefaultRules?: boolean;
|
|
165
199
|
messages?: {
|
|
166
200
|
required?: string;
|
|
@@ -178,6 +212,7 @@ export declare function createSchemaBuilder(options?: SchemaBuilderOptions): {
|
|
|
178
212
|
kinds: Readonly<Record<string, KindDef>>;
|
|
179
213
|
fieldTypes: Readonly<Record<string, FieldTypeDef<AnyObjectSchema>>>;
|
|
180
214
|
rules: Readonly<Record<string, RuleDef<AnyObjectSchema>>>;
|
|
215
|
+
formRules: Readonly<Record<string, FormRuleDef<AnyObjectSchema>>>;
|
|
181
216
|
};
|
|
182
217
|
/**
|
|
183
218
|
* Formisch's `initialInput` for a definition: each field's `default`, nested the way the value
|
|
@@ -598,6 +633,7 @@ export declare const defaultRules: {
|
|
|
598
633
|
readonly value: import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<undefined>, import("valibot").IntegerAction<number, undefined>, import("valibot").MinValueAction<number, 0, undefined>]>;
|
|
599
634
|
}, undefined>>;
|
|
600
635
|
};
|
|
636
|
+
export declare const defineFormRule: <T extends AnyObjectSchema>(def: FormRuleDef<T>) => FormRuleDef<T>;
|
|
601
637
|
export declare const defineRule: <T extends AnyObjectSchema>(def: RuleDef<T>) => RuleDef<T>;
|
|
602
638
|
/** Size and type of a File or Blob. */
|
|
603
639
|
export declare const fileRules: {
|
|
@@ -650,6 +686,19 @@ export declare const formatRules: {
|
|
|
650
686
|
readonly types: v.SchemaWithPipe<readonly [v.ArraySchema<v.PicklistSchema<readonly ["md4", "md5", "sha1", "sha256", "sha384", "sha512", "ripemd128", "ripemd160", "tiger128", "tiger160", "tiger192", "crc32", "crc32b", "adler32"], undefined>, undefined>, v.MinLengthAction<("md4" | "md5" | "sha1" | "sha256" | "sha384" | "sha512" | "ripemd128" | "ripemd160" | "tiger128" | "tiger160" | "tiger192" | "crc32" | "crc32b" | "adler32")[], 1, "Pick at least one hash type">]>;
|
|
651
687
|
}, undefined>>;
|
|
652
688
|
};
|
|
689
|
+
/** Every built-in form rule. */
|
|
690
|
+
export declare const defaultFormRules: {
|
|
691
|
+
sameAs: FormRuleDef<v.ObjectSchema<{
|
|
692
|
+
readonly other: v.StringSchema<undefined>;
|
|
693
|
+
}, undefined>>;
|
|
694
|
+
differentFrom: FormRuleDef<v.ObjectSchema<{
|
|
695
|
+
readonly other: v.StringSchema<undefined>;
|
|
696
|
+
}, undefined>>;
|
|
697
|
+
compare: FormRuleDef<v.ObjectSchema<{
|
|
698
|
+
readonly op: v.PicklistSchema<["lt", "lte", "gt", "gte"], undefined>;
|
|
699
|
+
readonly other: v.StringSchema<undefined>;
|
|
700
|
+
}, undefined>>;
|
|
701
|
+
};
|
|
653
702
|
/** Characters of a string, or items of an array */
|
|
654
703
|
export declare const lengthRules: {
|
|
655
704
|
length: RuleDef<v.ObjectSchema<{
|
|
@@ -744,5 +793,9 @@ export declare const RESERVED_FIELD_KEYS: readonly ["name", "type", "label", "de
|
|
|
744
793
|
* Rule keys this package owns. A rule's params can't use them.
|
|
745
794
|
*/
|
|
746
795
|
export declare const RESERVED_RULE_KEYS: readonly ["rule", "message"];
|
|
747
|
-
|
|
796
|
+
/**
|
|
797
|
+
* Form rule keys this package owns. A form rule's params can't use them.
|
|
798
|
+
*/
|
|
799
|
+
export declare const RESERVED_FORM_RULE_KEYS: readonly ["rule", "field", "message"];
|
|
800
|
+
export type { AnyAction, AnyObjectSchema, AnySchema, BuildContext, DefinitionIssue, DefinitionIssueCode, DefinitionPath, FieldTypeDef, FieldWithProps, FormRuleDef, JsonField, JsonForm, JsonFormRule, JsonRule, KindDef, KindOf, MessageContext, RuleCategory, RuleDef, RuleMeta, RuleStage, SchemaBuilderOptions, ValidateResult, ValueKind, WalkedField };
|
|
748
801
|
//# sourceMappingURL=index.d.ts.map
|