@k8ordo/form 0.1.1 → 1.1.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 +31 -23
- package/dist/async-check.d.mts +3 -4
- package/dist/derive/attributes.d.mts +43 -8
- package/dist/derive/attributes.mjs +74 -19
- package/dist/derive/form-fields.d.mts +6 -5
- package/dist/derive/form-fields.mjs +55 -23
- package/dist/derive/messages.d.mts +2 -3
- package/dist/derive/messages.mjs +8 -11
- package/dist/hidden-value.d.mts +4 -8
- package/dist/hidden-value.mjs +8 -9
- package/dist/index.d.mts +4 -4
- package/dist/parse/parse-form.d.mts +3 -4
- package/dist/parse/parse-form.mjs +25 -4
- package/dist/parse/paths.d.mts +4 -4
- package/dist/paths.d.mts +34 -4
- package/dist/rules/define-form.d.mts +4 -5
- package/dist/rules/rules.d.mts +26 -10
- package/dist/rules/rules.mjs +14 -7
- package/dist/schema/object-schema.d.mts +7 -6
- package/dist/schema/walk.d.mts +16 -8
- package/dist/schema/walk.mjs +77 -10
- package/dist/server.d.mts +3 -3
- package/dist/types.d.mts +38 -17
- package/dist/use-form.d.mts +32 -12
- package/dist/use-form.mjs +136 -22
- package/docs/GUIDE.md +469 -71
- package/docs/llms.txt +61 -15
- package/package.json +18 -17
|
@@ -27,6 +27,19 @@ const rowCounts = (formData, arrays) => {
|
|
|
27
27
|
}
|
|
28
28
|
return counts;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Whether the control submitted its own idea of "nothing". A number control
|
|
32
|
+
* sends `''`, a select left on its placeholder sends `''`, and a file control
|
|
33
|
+
* an unnamed empty File — none is a value anyone entered, and handing one to
|
|
34
|
+
* the schema is how a blank field becomes 0 (`z.coerce.number()` reads `''` as
|
|
35
|
+
* 0), an optional choice rejects the blank it allows, or an empty upload passes
|
|
36
|
+
* for a real one. They arrive as `undefined` instead, which is the same thing
|
|
37
|
+
* the derivation probed when it decided whether the field is required.
|
|
38
|
+
*/
|
|
39
|
+
const isUntouched = (kind, value) => {
|
|
40
|
+
if (kind === "number" || kind === "choice") return value === "";
|
|
41
|
+
return kind === "file" && value instanceof File && value.name === "" && value.size === 0;
|
|
42
|
+
};
|
|
30
43
|
const describeMissing = (missing) => {
|
|
31
44
|
const shown = missing.slice(0, 5).join(", ");
|
|
32
45
|
return missing.length > 5 ? `${shown} … ほか${String(missing.length - 5)}件` : shown;
|
|
@@ -46,6 +59,7 @@ const parseForm = (input, formData) => {
|
|
|
46
59
|
const raw = {};
|
|
47
60
|
const missing = [];
|
|
48
61
|
const secrets = /* @__PURE__ */ new Set();
|
|
62
|
+
const groups = /* @__PURE__ */ new Set();
|
|
49
63
|
const rows = rowCounts(formData, map.arrays);
|
|
50
64
|
for (const array of map.arrays) setPath(raw, array.path, Array.from({ length: rows[array.path] ?? 0 }));
|
|
51
65
|
const strings = (name) => formData.getAll(name).filter((value) => typeof value === "string");
|
|
@@ -54,32 +68,39 @@ const parseForm = (input, formData) => {
|
|
|
54
68
|
for (const name of names) {
|
|
55
69
|
if (leaf.json.input === "password") secrets.add(name);
|
|
56
70
|
if (leaf.group !== void 0) {
|
|
71
|
+
groups.add(name);
|
|
57
72
|
setPath(raw, name, strings(name));
|
|
58
73
|
continue;
|
|
59
74
|
}
|
|
60
|
-
if (leaf.
|
|
75
|
+
if (leaf.kind === "checkbox") {
|
|
61
76
|
setPath(raw, name, formData.has(name));
|
|
62
77
|
continue;
|
|
63
78
|
}
|
|
64
79
|
const entries = formData.getAll(name);
|
|
65
80
|
if (entries.length === 0) {
|
|
66
|
-
if (leaf.
|
|
81
|
+
if (leaf.kind === "choice" || leaf.kind === "string-checkbox") {
|
|
82
|
+
setPath(raw, name, void 0);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
67
85
|
missing.push(name);
|
|
68
86
|
continue;
|
|
69
87
|
}
|
|
70
|
-
|
|
88
|
+
const value = entries.length === 1 ? entries[0] : entries;
|
|
89
|
+
setPath(raw, name, isUntouched(leaf.kind, value) ? void 0 : value);
|
|
71
90
|
}
|
|
72
91
|
}
|
|
73
92
|
if (missing.length > 0) throw new Error(`[@k8ordo/form] スキーマにあるフィールドが送信されていません: ${describeMissing(missing)}\ninput に name が付いていないか、その入力欄が描画されていない可能性があります。`);
|
|
74
93
|
const result = asProbe(schema).safeParse(raw);
|
|
75
94
|
const breaches = {};
|
|
76
95
|
for (const rule of rules) {
|
|
96
|
+
if (Object.hasOwn(breaches, rule.field)) continue;
|
|
77
97
|
const message = breachOf(rule, strings);
|
|
78
98
|
if (message !== void 0) breaches[rule.field] = message;
|
|
79
99
|
}
|
|
80
100
|
const values = {};
|
|
101
|
+
for (const name of groups) values[name] = strings(name);
|
|
81
102
|
for (const [name, value] of formData.entries()) {
|
|
82
|
-
if (typeof value !== "string" || secrets.has(name)) continue;
|
|
103
|
+
if (typeof value !== "string" || secrets.has(name) || groups.has(name)) continue;
|
|
83
104
|
const existing = values[name];
|
|
84
105
|
if (existing === void 0) values[name] = value;
|
|
85
106
|
else if (typeof existing === "string") values[name] = [existing, value];
|
package/dist/parse/paths.d.mts
CHANGED
|
@@ -6,15 +6,15 @@ type Step = {
|
|
|
6
6
|
index: number;
|
|
7
7
|
};
|
|
8
8
|
/** Split `items[1].name` into its steps. */
|
|
9
|
-
declare const stepsOf: (name: string) => Step[];
|
|
9
|
+
export declare const stepsOf: (name: string) => Step[];
|
|
10
10
|
/**
|
|
11
11
|
* Write a value at a dotted/indexed path, creating the objects and arrays the
|
|
12
12
|
* path implies. Gaps in an array stay holes rather than shifting later rows
|
|
13
13
|
* onto earlier indexes, so a row removed on the client cannot silently
|
|
14
14
|
* renumber the rows that follow.
|
|
15
15
|
*/
|
|
16
|
-
declare const setPath: (root: Record<string, unknown>, name: string, value: unknown) => void;
|
|
16
|
+
export declare const setPath: (root: Record<string, unknown>, name: string, value: unknown) => void;
|
|
17
17
|
/** Turn a zod issue path into the `name` of the input that produced it. */
|
|
18
|
-
declare const nameOf: (path: readonly PropertyKey[]) => string;
|
|
18
|
+
export declare const nameOf: (path: readonly PropertyKey[]) => string;
|
|
19
19
|
//#endregion
|
|
20
|
-
export {
|
|
20
|
+
export {};
|
package/dist/paths.d.mts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* is a field.
|
|
8
8
|
*
|
|
9
9
|
* Wrappers (`.optional()`, `.nullable()`, `.default()`, …) are peeled the same
|
|
10
|
-
* way the runtime walk peels them, so the two advertise the same paths
|
|
10
|
+
* way the runtime walk peels them, so the two advertise the same paths — except
|
|
11
|
+
* where the walk refuses the schema outright, as it does a nullable object.
|
|
11
12
|
*/
|
|
12
13
|
type Join<Prefix extends string, Key extends string> = Prefix extends '' ? Key : `${Prefix}.${Key}`;
|
|
13
14
|
/**
|
|
@@ -42,15 +43,44 @@ type IsEnum<Schema> = Unwrap<Schema> extends {
|
|
|
42
43
|
};
|
|
43
44
|
};
|
|
44
45
|
} ? true : false;
|
|
45
|
-
type FieldPathsOf<Schema, Prefix extends string = ''> = Unwrap<Schema> extends (infer Bare) ? IsArray<Bare> extends true ? IsEnum<ArrayElement<Bare>> extends true ? Prefix extends '' ? never : Prefix : never : Bare extends {
|
|
46
|
+
export type FieldPathsOf<Schema, Prefix extends string = ''> = Unwrap<Schema> extends (infer Bare) ? IsArray<Bare> extends true ? IsEnum<ArrayElement<Bare>> extends true ? Prefix extends '' ? never : Prefix : never : Bare extends {
|
|
46
47
|
shape: infer Shape;
|
|
47
48
|
} ? { [Key in keyof Shape & string]: FieldPathsOf<Shape[Key], Join<Prefix, Key>>; }[keyof Shape & string] : Prefix extends '' ? never : Prefix : never;
|
|
48
49
|
/**
|
|
49
50
|
* No recursion into an array's element: a repeat inside a repeat has no
|
|
50
51
|
* unambiguous name, and the runtime walk refuses it too.
|
|
51
52
|
*/
|
|
52
|
-
type ArrayPathsOf<Schema, Prefix extends string = ''> = Unwrap<Schema> extends (infer Bare) ? IsArray<Bare> extends true ? IsEnum<ArrayElement<Bare>> extends true ? never : Prefix extends '' ? never : Prefix : Bare extends {
|
|
53
|
+
export type ArrayPathsOf<Schema, Prefix extends string = ''> = Unwrap<Schema> extends (infer Bare) ? IsArray<Bare> extends true ? IsEnum<ArrayElement<Bare>> extends true ? never : Prefix extends '' ? never : Prefix : Bare extends {
|
|
53
54
|
shape: infer Shape;
|
|
54
55
|
} ? { [Key in keyof Shape & string]: ArrayPathsOf<Shape[Key], Join<Prefix, Key>>; }[keyof Shape & string] : never : never;
|
|
56
|
+
/**
|
|
57
|
+
* The one control whose `input` carries a `value` is a checkbox whose schema
|
|
58
|
+
* reads the submitted string (`z.stringbool()`). The runtime walk finds it by
|
|
59
|
+
* encoding `true`, which takes a codec; the type sees the same codec as a
|
|
60
|
+
* string on its way in and a boolean on its way out. The string is read off
|
|
61
|
+
* the codec's inner schema because `zod/mini` leaves the codec's own input
|
|
62
|
+
* `unknown`.
|
|
63
|
+
*/
|
|
64
|
+
type EncodesTrueAsString<Schema> = Schema extends {
|
|
65
|
+
_zod: {
|
|
66
|
+
output: infer Output;
|
|
67
|
+
def: {
|
|
68
|
+
in: {
|
|
69
|
+
_zod: {
|
|
70
|
+
output: infer In;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
reverseTransform: unknown;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
} ? [In] extends [string] ? [Output] extends [boolean] ? true : false : false : false;
|
|
77
|
+
/**
|
|
78
|
+
* The field paths whose `input` carries the string a checked box submits.
|
|
79
|
+
* Arrays need no case of their own: neither a checkbox group nor repeated rows
|
|
80
|
+
* take a string in.
|
|
81
|
+
*/
|
|
82
|
+
export type StringCheckboxPathsOf<Schema, Prefix extends string = ''> = Unwrap<Schema> extends (infer Bare) ? Bare extends {
|
|
83
|
+
shape: infer Shape;
|
|
84
|
+
} ? { [Key in keyof Shape & string]: StringCheckboxPathsOf<Shape[Key], Join<Prefix, Key>>; }[keyof Shape & string] : Prefix extends '' ? never : EncodesTrueAsString<Bare> extends true ? Prefix : never : never;
|
|
55
85
|
//#endregion
|
|
56
|
-
export {
|
|
86
|
+
export {};
|
|
@@ -10,12 +10,11 @@ import { ObjectSchema } from "../schema/object-schema.mjs";
|
|
|
10
10
|
* The rules are typed against the schema's own paths, so
|
|
11
11
|
* `sameAs('confrim', …)` is a compile error, not a rule that never fires.
|
|
12
12
|
*/
|
|
13
|
-
type FormDefinition<Schema extends ObjectSchema = ObjectSchema> = {
|
|
13
|
+
export type FormDefinition<Schema extends ObjectSchema = ObjectSchema> = {
|
|
14
14
|
schema: Schema;
|
|
15
15
|
rules: Array<Rule<FieldPathsOf<Schema>>>;
|
|
16
16
|
};
|
|
17
|
-
declare const defineForm: <Schema extends ObjectSchema>(schema: Schema, rules?: Array<Rule<FieldPathsOf<Schema>>>) => FormDefinition<Schema>;
|
|
17
|
+
export declare const defineForm: <Schema extends ObjectSchema>(schema: Schema, rules?: Array<Rule<FieldPathsOf<Schema>>>) => FormDefinition<Schema>;
|
|
18
18
|
/** Accept either a bare schema or a definition, so rules stay optional. */
|
|
19
|
-
declare const asDefinition: <Schema extends ObjectSchema>(input: FormDefinition<Schema> | Schema) => FormDefinition<Schema>;
|
|
20
|
-
//#endregion
|
|
21
|
-
export { FormDefinition, asDefinition, defineForm };
|
|
19
|
+
export declare const asDefinition: <Schema extends ObjectSchema>(input: FormDefinition<Schema> | Schema) => FormDefinition<Schema>;
|
|
20
|
+
//#endregion
|
package/dist/rules/rules.d.mts
CHANGED
|
@@ -10,32 +10,48 @@
|
|
|
10
10
|
* it, so a typo in a field name fails to compile instead of producing a rule
|
|
11
11
|
* that never fires.
|
|
12
12
|
*/
|
|
13
|
-
type Rule<Field extends string = string> =
|
|
13
|
+
export type Rule<Field extends string = string> = RuleOf<Field, RuleMessage>;
|
|
14
|
+
/**
|
|
15
|
+
* The text a broken rule reports, or a function returning it. Like zod's
|
|
16
|
+
* `{ error: () => … }`, the function is called when the rule is reported —
|
|
17
|
+
* `formFields` calls it as it derives the fields, `parseForm` when a rule
|
|
18
|
+
* breaks — so a message that follows the request (its locale, say) is
|
|
19
|
+
* declared once and read in whichever request reports it.
|
|
20
|
+
*/
|
|
21
|
+
export type RuleMessage = string | (() => string);
|
|
22
|
+
/**
|
|
23
|
+
* A rule as it crosses to the client. A function cannot cross the RSC
|
|
24
|
+
* boundary, so `formFields` has already called it.
|
|
25
|
+
*/
|
|
26
|
+
export type DerivedRule = RuleOf<string, string>;
|
|
27
|
+
type RuleOf<Field extends string, Message> = {
|
|
14
28
|
kind: 'sameAs';
|
|
15
29
|
field: Field;
|
|
16
30
|
other: Field;
|
|
17
|
-
message:
|
|
31
|
+
message: Message;
|
|
18
32
|
} | {
|
|
19
33
|
kind: 'minChecked';
|
|
20
34
|
field: Field;
|
|
21
35
|
min: number;
|
|
22
|
-
message:
|
|
36
|
+
message: Message;
|
|
23
37
|
} | {
|
|
24
38
|
kind: 'requiredWhen';
|
|
25
39
|
field: Field;
|
|
26
40
|
when: Field;
|
|
27
41
|
equals: string;
|
|
28
|
-
message:
|
|
42
|
+
message: Message;
|
|
29
43
|
};
|
|
30
44
|
/** Reads the current values of a name, however many controls carry it. */
|
|
31
|
-
type Values = (name: string) => string[];
|
|
45
|
+
export type Values = (name: string) => string[];
|
|
32
46
|
/** The value must equal another field's. Password confirmation, and the like. */
|
|
33
|
-
declare const sameAs: <Field extends string>(field: Field, other: Field, message:
|
|
47
|
+
export declare const sameAs: <Field extends string>(field: Field, other: Field, message: RuleMessage) => Rule<Field>;
|
|
34
48
|
/** At least `min` boxes sharing this name must be checked. */
|
|
35
|
-
declare const minChecked: <Field extends string>(field: Field, min: number, message:
|
|
49
|
+
export declare const minChecked: <Field extends string>(field: Field, min: number, message: RuleMessage) => Rule<Field>;
|
|
36
50
|
/** Required only while another field holds a particular value. */
|
|
37
|
-
declare const requiredWhen: <Field extends string>(field: Field, when: Field, equals: string, message:
|
|
51
|
+
export declare const requiredWhen: <Field extends string>(field: Field, when: Field, equals: string, message: RuleMessage) => Rule<Field>;
|
|
52
|
+
/** The rule with its message called, ready to cross to the client. */
|
|
53
|
+
export declare const deriveRule: (rule: Rule) => DerivedRule;
|
|
38
54
|
/** The message when the rule is broken, or undefined when it holds. */
|
|
39
|
-
declare const breachOf: (rule: Rule, values: Values) => string | undefined;
|
|
55
|
+
export declare const breachOf: (rule: Rule, values: Values) => string | undefined;
|
|
40
56
|
//#endregion
|
|
41
|
-
export {
|
|
57
|
+
export {};
|
package/dist/rules/rules.mjs
CHANGED
|
@@ -22,14 +22,21 @@ const requiredWhen = (field, when, equals, message) => ({
|
|
|
22
22
|
message
|
|
23
23
|
});
|
|
24
24
|
const first = (values, name) => values(name)[0] ?? "";
|
|
25
|
-
|
|
26
|
-
const breachOf = (rule, values) => {
|
|
25
|
+
const holds = (rule, values) => {
|
|
27
26
|
switch (rule.kind) {
|
|
28
|
-
case "sameAs": return first(values, rule.field) === first(values, rule.other)
|
|
29
|
-
case "minChecked": return values(rule.field).length >= rule.min
|
|
30
|
-
case "requiredWhen": return first(values, rule.when)
|
|
31
|
-
default: return;
|
|
27
|
+
case "sameAs": return first(values, rule.field) === first(values, rule.other);
|
|
28
|
+
case "minChecked": return values(rule.field).length >= rule.min;
|
|
29
|
+
case "requiredWhen": return first(values, rule.when) !== rule.equals || first(values, rule.field) !== "";
|
|
30
|
+
default: return true;
|
|
32
31
|
}
|
|
33
32
|
};
|
|
33
|
+
const textOf = (message) => typeof message === "function" ? message() : message;
|
|
34
|
+
/** The rule with its message called, ready to cross to the client. */
|
|
35
|
+
const deriveRule = (rule) => ({
|
|
36
|
+
...rule,
|
|
37
|
+
message: textOf(rule.message)
|
|
38
|
+
});
|
|
39
|
+
/** The message when the rule is broken, or undefined when it holds. */
|
|
40
|
+
const breachOf = (rule, values) => holds(rule, values) ? void 0 : textOf(rule.message);
|
|
34
41
|
//#endregion
|
|
35
|
-
export { breachOf, minChecked, requiredWhen, sameAs };
|
|
42
|
+
export { breachOf, deriveRule, minChecked, requiredWhen, sameAs };
|
|
@@ -6,16 +6,18 @@ import { $ZodObject, $ZodShape, $ZodType } from "zod/v4/core";
|
|
|
6
6
|
* `ZodObject` is what lets a caller pick the seven-times-smaller entry without
|
|
7
7
|
* losing the types.
|
|
8
8
|
*/
|
|
9
|
-
type ObjectSchema<Shape extends $ZodShape = $ZodShape> = $ZodObject<Shape> & {
|
|
9
|
+
export type ObjectSchema<Shape extends $ZodShape = $ZodShape> = $ZodObject<Shape> & {
|
|
10
10
|
shape: Shape;
|
|
11
11
|
};
|
|
12
|
-
type ProbeIssue = {
|
|
12
|
+
export type ProbeIssue = {
|
|
13
13
|
code: string;
|
|
14
|
+
/** Which string format rejected the value, on an `invalid_format` issue. */
|
|
15
|
+
format?: string;
|
|
14
16
|
message: string;
|
|
15
17
|
path: readonly PropertyKey[];
|
|
16
18
|
};
|
|
17
19
|
/** What probing needs, present on both entries but not on the core type. */
|
|
18
|
-
type ProbeSchema = $ZodType & {
|
|
20
|
+
export type ProbeSchema = $ZodType & {
|
|
19
21
|
safeParse: (value: unknown) => {
|
|
20
22
|
success: true;
|
|
21
23
|
data: unknown;
|
|
@@ -28,6 +30,5 @@ type ProbeSchema = $ZodType & {
|
|
|
28
30
|
};
|
|
29
31
|
};
|
|
30
32
|
};
|
|
31
|
-
declare const asProbe: (schema: $ZodType) => ProbeSchema;
|
|
32
|
-
//#endregion
|
|
33
|
-
export { ObjectSchema, ProbeIssue, ProbeSchema, asProbe };
|
|
33
|
+
export declare const asProbe: (schema: $ZodType) => ProbeSchema;
|
|
34
|
+
//#endregion
|
package/dist/schema/walk.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { LeafSchema } from "../derive/attributes.mjs";
|
|
1
|
+
import { ControlKind, LeafSchema } from "../derive/attributes.mjs";
|
|
2
2
|
import { ObjectSchema } from "./object-schema.mjs";
|
|
3
3
|
import { $ZodType } from "zod/v4/core";
|
|
4
4
|
//#region src/schema/walk.d.ts
|
|
5
5
|
/** Placeholder an array item's name carries until a row index is known. */
|
|
6
|
-
declare const INDEX = "{index}";
|
|
7
|
-
type LeafNode = {
|
|
6
|
+
export declare const INDEX = "{index}";
|
|
7
|
+
export type LeafNode = {
|
|
8
8
|
/** Dotted path within the parsed object, e.g. `user.email`. */
|
|
9
9
|
path: string;
|
|
10
10
|
/** The `name` attribute, with {@link INDEX} still in it for array items. */
|
|
@@ -13,6 +13,9 @@ type LeafNode = {
|
|
|
13
13
|
input?: string;
|
|
14
14
|
};
|
|
15
15
|
zod: $ZodType;
|
|
16
|
+
kind: ControlKind;
|
|
17
|
+
/** `string-checkbox` がチェック時に送る文字列({@link checkedValueOf})。 */
|
|
18
|
+
checkedValue?: string;
|
|
16
19
|
required: boolean;
|
|
17
20
|
/** The array this leaf repeats inside, or null. */
|
|
18
21
|
arrayPath: string | null;
|
|
@@ -28,21 +31,26 @@ type LeafNode = {
|
|
|
28
31
|
maxItems?: number;
|
|
29
32
|
};
|
|
30
33
|
};
|
|
31
|
-
type ArrayNode = {
|
|
34
|
+
export type ArrayNode = {
|
|
32
35
|
path: string;
|
|
33
36
|
name: string;
|
|
34
37
|
minItems?: number;
|
|
35
38
|
maxItems?: number;
|
|
36
39
|
};
|
|
37
|
-
type SchemaMap = {
|
|
40
|
+
export type SchemaMap = {
|
|
38
41
|
leaves: LeafNode[];
|
|
39
42
|
arrays: ArrayNode[];
|
|
40
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* Peel wrapper schemas until the node that owns the structure is reached.
|
|
46
|
+
* `toJSONSchema` flattens `.optional()` / `.default()` into the plain node, so
|
|
47
|
+
* pairing the two trees requires flattening the zod side the same way.
|
|
48
|
+
*/
|
|
49
|
+
export declare const unwrap: ($schema: $ZodType) => $ZodType;
|
|
41
50
|
/**
|
|
42
51
|
* Describe a schema once: every leaf that becomes an input, and every array
|
|
43
52
|
* that becomes a repeatable group. Both the derivation and the parse read this,
|
|
44
53
|
* so the two can never disagree about what the form contains.
|
|
45
54
|
*/
|
|
46
|
-
declare const schemaMap: (schema: ObjectSchema) => SchemaMap;
|
|
47
|
-
//#endregion
|
|
48
|
-
export { ArrayNode, INDEX, LeafNode, SchemaMap, schemaMap };
|
|
55
|
+
export declare const schemaMap: (schema: ObjectSchema) => SchemaMap;
|
|
56
|
+
//#endregion
|
package/dist/schema/walk.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { asProbe } from "./object-schema.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { controlKindOf, emptySubmissionOf } from "../derive/attributes.mjs";
|
|
3
|
+
import { safeEncode, toJSONSchema } from "zod/v4/core";
|
|
3
4
|
//#region src/schema/walk.ts
|
|
4
5
|
/** Placeholder an array item's name carries until a row index is known. */
|
|
5
6
|
const INDEX = "{index}";
|
|
@@ -23,15 +24,74 @@ const elementOf = ($schema) => {
|
|
|
23
24
|
return candidate.element ?? candidate._zod?.def?.element;
|
|
24
25
|
};
|
|
25
26
|
/**
|
|
27
|
+
* BigInt has no JSON Schema form, so its node arrives empty and reads as a
|
|
28
|
+
* text control. It does render as one, but a blank one is no more a value than
|
|
29
|
+
* a blank number is — `z.coerce.bigint()` reads `''` as 0n — so its submission
|
|
30
|
+
* is judged the way a number's is.
|
|
31
|
+
*/
|
|
32
|
+
const kindOf = (json, $schema) => {
|
|
33
|
+
const kind = controlKindOf(json);
|
|
34
|
+
return kind === "text" && unwrap($schema)._zod?.def?.type === "bigint" ? "number" : kind;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* 文字列を読むスキーマのチェックボックスが、チェック時に送る文字列。スキーマ
|
|
38
|
+
* 自身が `true` を encode した綴りを使う。ブラウザ既定の `on` を拒む
|
|
39
|
+
* `z.stringbool({ truthy: ['yes'] })` にも読める値が届き、`@k8ordo/state` が
|
|
40
|
+
* `true` として URL に書く文字列とも一致する。
|
|
41
|
+
*/
|
|
42
|
+
const checkedValueOf = ($schema) => {
|
|
43
|
+
try {
|
|
44
|
+
const encoded = safeEncode($schema, true);
|
|
45
|
+
return encoded.success && typeof encoded.data === "string" ? encoded.data : void 0;
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
26
51
|
* `required` in JSON Schema means "the key is present", but a form always
|
|
27
|
-
* submits something for every control
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* attribute mean the same on both sides.
|
|
52
|
+
* submits something for every control, and what that is depends on the control
|
|
53
|
+
* — `emptySubmissionOf` is the one place that decides. Asking the schema what
|
|
54
|
+
* it does with that exact value, which is also what the parse will hand it, is
|
|
55
|
+
* what makes the attribute mean the same on both sides.
|
|
56
|
+
*/
|
|
57
|
+
const rejectsEmptySubmission = (kind, $schema) => !asProbe($schema).safeParse(emptySubmissionOf(kind)).success;
|
|
58
|
+
/**
|
|
59
|
+
* Strings a text control could plausibly submit. One is never enough: a schema
|
|
60
|
+
* that coerces reads some strings and not others, and rejecting the one probe
|
|
61
|
+
* that happens to be nonsense says nothing about the rest —
|
|
62
|
+
* `z.coerce.date()` refuses a word and accepts a date.
|
|
31
63
|
*/
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
64
|
+
const TEXT_PROBES = [
|
|
65
|
+
"1",
|
|
66
|
+
"2000-01-01",
|
|
67
|
+
"k8ordo-probe"
|
|
68
|
+
];
|
|
69
|
+
const rejectsAsWrongType = ($schema, value) => {
|
|
70
|
+
const probed = asProbe($schema).safeParse(value);
|
|
71
|
+
return !probed.success && probed.error.issues.some((issue) => issue.code === "invalid_type");
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* A text or number control can only submit strings, and the parse reads a
|
|
75
|
+
* checkbox as a boolean unless its schema names a string to submit, so a leaf
|
|
76
|
+
* that turns every such value away on type alone can never be satisfied by the
|
|
77
|
+
* control it derives. That is not a check the client skips; it is a form that
|
|
78
|
+
* always fails, so it is refused here rather than shipped, with the reason. (A
|
|
79
|
+
* file is exempt: the parse hands the schema a File, which is what `z.file()`
|
|
80
|
+
* reads.)
|
|
81
|
+
*
|
|
82
|
+
* A constraint is not a refusal: `z.coerce.number().min(100)` rejects the probe
|
|
83
|
+
* with `too_small`, which is the schema doing its job.
|
|
84
|
+
*/
|
|
85
|
+
const refusalOf = (json, $schema, kind) => {
|
|
86
|
+
if (kind === "string-checkbox") return asProbe($schema).safeParse(void 0).data === true ? "チェックを外したボックスは何も送らないため、未送信を true と読むこのスキーマ(.default(true) など)では false を送れません。チェックすると true になる向きの項目にしてください" : void 0;
|
|
87
|
+
const control = controlKindOf(json);
|
|
88
|
+
if (control === "number") {
|
|
89
|
+
const asText = asProbe($schema).safeParse("1");
|
|
90
|
+
if (asText.success) return;
|
|
91
|
+
return asProbe($schema).safeParse(1).success || asText.error.issues.some((issue) => issue.code === "invalid_type") ? "フォームの値は文字列で届くため、このスキーマはどんな入力でも失敗します。z.coerce.number() を使ってください" : void 0;
|
|
92
|
+
}
|
|
93
|
+
if (kind === "checkbox") return [true, false].every((value) => rejectsAsWrongType($schema, value)) ? "チェックボックスは真偽値(z.boolean())か、送信する文字列(z.stringbool())として読むため、どちらでもないこのスキーマはどんな入力でも失敗します" : void 0;
|
|
94
|
+
if (control === "text" && json.type !== "string") return TEXT_PROBES.every((probe) => rejectsAsWrongType($schema, probe)) ? "このスキーマは文字列を受け付けないため、フォームが送信できる値がありません(z.date() や z.bigint() などは表現できません)" : void 0;
|
|
35
95
|
};
|
|
36
96
|
const fail = (path, reason) => {
|
|
37
97
|
throw new Error(`[@k8ordo/form] '${path === "" ? "(schema)" : path}': ${reason}`);
|
|
@@ -61,6 +121,7 @@ const walkNode = (json, zod, path, name, context, out) => {
|
|
|
61
121
|
name,
|
|
62
122
|
json: json.items,
|
|
63
123
|
zod: element,
|
|
124
|
+
kind: controlKindOf(json.items),
|
|
64
125
|
required: false,
|
|
65
126
|
arrayPath: context.arrayPath,
|
|
66
127
|
itemKey: context.itemKey,
|
|
@@ -89,12 +150,18 @@ const walkNode = (json, zod, path, name, context, out) => {
|
|
|
89
150
|
if (json.anyOf !== void 0) {
|
|
90
151
|
for (const branch of json.anyOf) if (branch.type === "object" || branch.type === "array") fail(path, "nullable / union のオブジェクトや配列は、どちらの枝を描画すべきか決まらないため表現できません");
|
|
91
152
|
}
|
|
153
|
+
const checkedValue = controlKindOf(json) === "checkbox" ? checkedValueOf(zod) : void 0;
|
|
154
|
+
const kind = checkedValue === void 0 ? kindOf(json, zod) : "string-checkbox";
|
|
155
|
+
const refusal = refusalOf(json, zod, kind);
|
|
156
|
+
if (refusal !== void 0) fail(path, refusal);
|
|
92
157
|
out.leaves.push({
|
|
93
158
|
path,
|
|
94
159
|
name,
|
|
95
160
|
json,
|
|
96
161
|
zod,
|
|
97
|
-
|
|
162
|
+
kind,
|
|
163
|
+
checkedValue,
|
|
164
|
+
required: rejectsEmptySubmission(kind, zod),
|
|
98
165
|
arrayPath: context.arrayPath,
|
|
99
166
|
itemKey: context.itemKey
|
|
100
167
|
});
|
|
@@ -125,4 +192,4 @@ const schemaMap = (schema) => {
|
|
|
125
192
|
return out;
|
|
126
193
|
};
|
|
127
194
|
//#endregion
|
|
128
|
-
export { INDEX, schemaMap };
|
|
195
|
+
export { INDEX, schemaMap, unwrap };
|
package/dist/server.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Rule, minChecked, requiredWhen, sameAs } from "./rules/rules.mjs";
|
|
2
|
-
import { DerivedArray, DerivedField, DroppedCheck, FieldInput, FormFields, FormState, ValidityFlag } from "./types.mjs";
|
|
1
|
+
import { DerivedRule, Rule, RuleMessage, minChecked, requiredWhen, sameAs } from "./rules/rules.mjs";
|
|
2
|
+
import { DerivedArray, DerivedField, DroppedCheck, FieldInput, FormFields, FormState, StringCheckboxInput, ValidityFlag } from "./types.mjs";
|
|
3
3
|
import { FormDefinition, defineForm } from "./rules/define-form.mjs";
|
|
4
4
|
import { formFields } from "./derive/form-fields.mjs";
|
|
5
5
|
import { ParseResult, parseForm } from "./parse/parse-form.mjs";
|
|
6
|
-
export { type DerivedArray, type DerivedField, type DroppedCheck, type FieldInput, type FormDefinition, type FormFields, type FormState, type ParseResult, type Rule, type ValidityFlag, defineForm, formFields, minChecked, parseForm, requiredWhen, sameAs };
|
|
6
|
+
export { type DerivedArray, type DerivedField, type DerivedRule, type DroppedCheck, type FieldInput, type FormDefinition, type FormFields, type FormState, type ParseResult, type Rule, type RuleMessage, type StringCheckboxInput, type ValidityFlag, defineForm, formFields, minChecked, parseForm, requiredWhen, sameAs };
|
package/dist/types.d.mts
CHANGED
|
@@ -1,27 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DerivedRule } from "./rules/rules.mjs";
|
|
2
2
|
//#region src/types.d.ts
|
|
3
3
|
/** ValidityState flags a derived field can report a message for. */
|
|
4
|
-
type ValidityFlag = 'badInput' | 'patternMismatch' | 'rangeOverflow' | 'rangeUnderflow' | 'stepMismatch' | 'tooLong' | 'tooShort' | 'typeMismatch' | 'valueMissing';
|
|
4
|
+
export type ValidityFlag = 'badInput' | 'patternMismatch' | 'rangeOverflow' | 'rangeUnderflow' | 'stepMismatch' | 'tooLong' | 'tooShort' | 'typeMismatch' | 'valueMissing';
|
|
5
5
|
/** Attributes spread onto the input element itself. */
|
|
6
|
-
type FieldInput = {
|
|
6
|
+
export type FieldInput = {
|
|
7
7
|
name: string;
|
|
8
8
|
type?: string;
|
|
9
9
|
required?: boolean;
|
|
10
10
|
minLength?: number;
|
|
11
11
|
maxLength?: number;
|
|
12
12
|
pattern?: string;
|
|
13
|
+
/** Comma-separated mime types for a file control, from `.mime()`. */
|
|
14
|
+
accept?: string;
|
|
13
15
|
min?: number | string;
|
|
14
16
|
max?: number | string;
|
|
15
17
|
step?: number | 'any';
|
|
16
18
|
defaultValue?: string;
|
|
17
19
|
defaultChecked?: boolean;
|
|
18
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* The input of a checkbox whose schema reads the submitted string
|
|
23
|
+
* (`z.stringbool()`): it also carries what a checked box submits.
|
|
24
|
+
*/
|
|
25
|
+
export type StringCheckboxInput = FieldInput & {
|
|
26
|
+
/** The schema's own spelling of `true`. */
|
|
27
|
+
value?: string;
|
|
28
|
+
};
|
|
19
29
|
/**
|
|
20
30
|
* One field as derived from the schema. Serializable on purpose: it crosses the
|
|
21
31
|
* RSC boundary as props, which is what keeps zod out of the client bundle.
|
|
22
32
|
*/
|
|
23
|
-
type DerivedField = {
|
|
24
|
-
input:
|
|
33
|
+
export type DerivedField<Input extends FieldInput = FieldInput> = {
|
|
34
|
+
input: Input;
|
|
25
35
|
/** Message per ValidityState flag, taken from zod itself. */
|
|
26
36
|
messages: Partial<Record<ValidityFlag, string>>;
|
|
27
37
|
/** True when the input must not be echoed back after a failed submit. */
|
|
@@ -31,7 +41,7 @@ type DerivedField = {
|
|
|
31
41
|
* A repeatable group. The item is described once; the row index is filled into
|
|
32
42
|
* the name when a row renders, because rows do not exist until then.
|
|
33
43
|
*/
|
|
34
|
-
type DerivedArray = {
|
|
44
|
+
export type DerivedArray = {
|
|
35
45
|
path: string;
|
|
36
46
|
minItems?: number;
|
|
37
47
|
maxItems?: number;
|
|
@@ -39,27 +49,39 @@ type DerivedArray = {
|
|
|
39
49
|
item: Record<string, DerivedField>;
|
|
40
50
|
};
|
|
41
51
|
/** What a constraint-carrying check lost on its way to HTML. */
|
|
42
|
-
type DroppedCheck = {
|
|
52
|
+
export type DroppedCheck = {
|
|
43
53
|
field: string;
|
|
44
54
|
reason: string;
|
|
45
55
|
};
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
/**
|
|
57
|
+
* `StringCheckboxPath` names the fields whose `input` carries `value`.
|
|
58
|
+
* `formFields` fills it in from the schema; a type written by hand that leaves
|
|
59
|
+
* it out still takes the derived fields, and only hides `value` from `field()`.
|
|
60
|
+
*/
|
|
61
|
+
export type FormFields<FieldPath extends string = string, ArrayPath extends string = string, StringCheckboxPath extends string = never> = {
|
|
62
|
+
fields: Record<FieldPath, DerivedField> & Record<StringCheckboxPath, DerivedField<StringCheckboxInput>>;
|
|
48
63
|
arrays: Record<ArrayPath, DerivedArray>;
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Checks HTML has no attribute for, each run by the same evaluator on both
|
|
66
|
+
* sides. A function message has already been called.
|
|
67
|
+
*/
|
|
68
|
+
rules: DerivedRule[];
|
|
69
|
+
/**
|
|
70
|
+
* Checks that HTML cannot express, returned instead of silently discarded.
|
|
71
|
+
* Not every such check is listed yet; the guide names the gaps.
|
|
72
|
+
*/
|
|
52
73
|
dropped: DroppedCheck[];
|
|
53
74
|
};
|
|
54
75
|
/**
|
|
55
76
|
* What a Server Action hands back to the form. Keys are dotted paths, so an
|
|
56
77
|
* error on a row reads as `items[1].name`.
|
|
57
78
|
*/
|
|
58
|
-
type FormState = {
|
|
79
|
+
export type FormState = {
|
|
59
80
|
errors?: Record<string, string>;
|
|
60
81
|
/**
|
|
61
|
-
* Submitted values for re-render. Secret fields are never
|
|
62
|
-
*
|
|
82
|
+
* Submitted values for re-render. Secret fields and files are never
|
|
83
|
+
* included; a checkbox group echoes as an array however many boxes were
|
|
84
|
+
* checked, and any other name submitted more than once as an array too.
|
|
63
85
|
*/
|
|
64
86
|
values?: Record<string, string | string[]>;
|
|
65
87
|
/** How many rows each array had, so a no-JS retry rebuilds them. */
|
|
@@ -72,5 +94,4 @@ type FormState = {
|
|
|
72
94
|
*/
|
|
73
95
|
token?: string;
|
|
74
96
|
};
|
|
75
|
-
//#endregion
|
|
76
|
-
export { DerivedArray, DerivedField, DroppedCheck, FieldInput, FormFields, FormState, ValidityFlag };
|
|
97
|
+
//#endregion
|