@oomfware/forms 0.1.1 → 0.2.1
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/dist/index.d.mts +148 -42
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +14 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +2 -1
- package/src/lib/form-utils.ts +13 -79
- package/src/lib/form.ts +2 -77
- package/src/lib/middleware.ts +17 -6
- package/src/lib/types.ts +241 -0
package/dist/index.d.mts
CHANGED
|
@@ -1,18 +1,161 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Middleware } from "@oomfware/fetch-router";
|
|
2
2
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
3
3
|
|
|
4
4
|
//#region src/lib/types.d.ts
|
|
5
5
|
type MaybePromise<T> = T | Promise<T>;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
type MaybeArray<T> = T | T[];
|
|
7
|
+
/**
|
|
8
|
+
* valid structure for form input data
|
|
9
|
+
*/
|
|
8
10
|
interface FormInput {
|
|
9
11
|
[key: string]: MaybeArray<string | number | boolean | File | FormInput>;
|
|
10
12
|
}
|
|
11
|
-
|
|
13
|
+
/**
|
|
14
|
+
* a validation issue with path information
|
|
15
|
+
*/
|
|
12
16
|
interface FormIssue {
|
|
13
17
|
message: string;
|
|
14
18
|
path: (string | number)[];
|
|
15
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* maps input types to their corresponding value types
|
|
22
|
+
*/
|
|
23
|
+
type InputTypeMap = {
|
|
24
|
+
text: string;
|
|
25
|
+
email: string;
|
|
26
|
+
password: string;
|
|
27
|
+
url: string;
|
|
28
|
+
tel: string;
|
|
29
|
+
search: string;
|
|
30
|
+
number: number;
|
|
31
|
+
range: number;
|
|
32
|
+
date: string;
|
|
33
|
+
'datetime-local': string;
|
|
34
|
+
time: string;
|
|
35
|
+
month: string;
|
|
36
|
+
week: string;
|
|
37
|
+
color: string;
|
|
38
|
+
checkbox: boolean | string[];
|
|
39
|
+
radio: string;
|
|
40
|
+
file: File;
|
|
41
|
+
'file multiple': File[];
|
|
42
|
+
hidden: string;
|
|
43
|
+
submit: string;
|
|
44
|
+
button: string;
|
|
45
|
+
reset: string;
|
|
46
|
+
image: string;
|
|
47
|
+
select: string;
|
|
48
|
+
'select multiple': string[];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* valid input types for a given value type
|
|
52
|
+
*/
|
|
53
|
+
type FieldInputType<T> = { [K in keyof InputTypeMap]: T extends InputTypeMap[K] ? K : never }[keyof InputTypeMap];
|
|
54
|
+
/**
|
|
55
|
+
* arguments for the `as()` method based on input type and field value
|
|
56
|
+
*/
|
|
57
|
+
type AsArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox' ? Value extends string[] ? [type: Type, value: Value[number] | (string & {})] : [type: Type] : Type extends 'radio' | 'submit' | 'hidden' ? [type: Type, value: Value | (string & {})] : [type: Type];
|
|
58
|
+
interface CheckboxRadioProps<T extends 'checkbox' | 'radio'> {
|
|
59
|
+
name: string;
|
|
60
|
+
type: T;
|
|
61
|
+
value: string;
|
|
62
|
+
'aria-invalid'?: 'true';
|
|
63
|
+
readonly checked: boolean;
|
|
64
|
+
}
|
|
65
|
+
interface FileProps {
|
|
66
|
+
name: string;
|
|
67
|
+
type: 'file';
|
|
68
|
+
'aria-invalid'?: 'true';
|
|
69
|
+
}
|
|
70
|
+
interface FileMultipleProps {
|
|
71
|
+
name: string;
|
|
72
|
+
type: 'file';
|
|
73
|
+
multiple: true;
|
|
74
|
+
'aria-invalid'?: 'true';
|
|
75
|
+
}
|
|
76
|
+
interface SelectProps {
|
|
77
|
+
name: string;
|
|
78
|
+
multiple: false;
|
|
79
|
+
'aria-invalid'?: 'true';
|
|
80
|
+
readonly value: string;
|
|
81
|
+
}
|
|
82
|
+
interface SelectMultipleProps {
|
|
83
|
+
name: string;
|
|
84
|
+
multiple: true;
|
|
85
|
+
'aria-invalid'?: 'true';
|
|
86
|
+
readonly value: string[];
|
|
87
|
+
}
|
|
88
|
+
interface TextProps {
|
|
89
|
+
name: string;
|
|
90
|
+
'aria-invalid'?: 'true';
|
|
91
|
+
readonly value: string;
|
|
92
|
+
}
|
|
93
|
+
interface TypedInputProps<T extends string> {
|
|
94
|
+
name: string;
|
|
95
|
+
type: T;
|
|
96
|
+
'aria-invalid'?: 'true';
|
|
97
|
+
readonly value: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* input element properties based on input type
|
|
101
|
+
*/
|
|
102
|
+
type InputElementProps<T extends keyof InputTypeMap> = T extends 'checkbox' | 'radio' ? CheckboxRadioProps<T> : T extends 'file' ? FileProps : T extends 'file multiple' ? FileMultipleProps : T extends 'select' ? SelectProps : T extends 'select multiple' ? SelectMultipleProps : T extends 'text' ? TextProps : TypedInputProps<T>;
|
|
103
|
+
/** valid leaf value types for form fields */
|
|
104
|
+
type FormFieldValue = string | string[] | number | boolean | File | File[];
|
|
105
|
+
/** guard to prevent infinite recursion when T is unknown or has an index signature */
|
|
106
|
+
type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
|
|
107
|
+
/** base methods available on all form fields */
|
|
108
|
+
interface FieldMethods<T> {
|
|
109
|
+
/** get the current value */
|
|
110
|
+
value(): T | undefined;
|
|
111
|
+
/** set the value */
|
|
112
|
+
set(value: T): T;
|
|
113
|
+
/** get validation issues for this field */
|
|
114
|
+
issues(): FormIssue[] | undefined;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* leaf field type for primitive values with `.as()` method
|
|
118
|
+
*/
|
|
119
|
+
type FormFieldLeaf<T extends FormFieldValue> = FieldMethods<T> & {
|
|
120
|
+
/**
|
|
121
|
+
* get props for binding to an input element.
|
|
122
|
+
* returns an object with `name`, `aria-invalid`, and type-specific props.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* <input {...fields.name.as('text')} />
|
|
127
|
+
* <input {...fields.age.as('number')} />
|
|
128
|
+
* <input {...fields.agreed.as('checkbox')} />
|
|
129
|
+
* <input {...fields.color.as('radio', 'red')} />
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
as<K$1 extends FieldInputType<T>>(...args: AsArgs<K$1, T>): InputElementProps<K$1>;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* container field type for objects/arrays with `.allIssues()` method
|
|
136
|
+
*/
|
|
137
|
+
type FormFieldContainer<T> = FieldMethods<T> & {
|
|
138
|
+
/** get all issues for this field and descendants */
|
|
139
|
+
allIssues(): FormIssue[] | undefined;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* fallback field type when recursion would be infinite
|
|
143
|
+
*/
|
|
144
|
+
type FormFieldUnknown<T> = FieldMethods<T> & {
|
|
145
|
+
/** get all issues for this field and descendants */
|
|
146
|
+
allIssues(): FormIssue[] | undefined;
|
|
147
|
+
/** get props for an input element */
|
|
148
|
+
as<K$1 extends FieldInputType<FormFieldValue>>(...args: AsArgs<K$1, FormFieldValue>): InputElementProps<K$1>;
|
|
149
|
+
} & {
|
|
150
|
+
[key: string | number]: FormFieldUnknown<unknown>;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* recursive type to build form fields structure with proxy access.
|
|
154
|
+
* preserves type information through the object hierarchy.
|
|
155
|
+
*/
|
|
156
|
+
type FormFields<T> = T extends void ? Record<string, never> : WillRecurseIndefinitely<T> extends true ? FormFieldUnknown<T> : NonNullable<T> extends string | number | boolean | File ? FormFieldLeaf<NonNullable<T>> : T extends string[] | File[] ? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> } : T extends Array<infer U> ? FormFieldContainer<T> & { [K in number]: FormFields<U> } : FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/lib/form.d.ts
|
|
16
159
|
/**
|
|
17
160
|
* the issue creator proxy passed to form callbacks.
|
|
18
161
|
* allows creating field-specific validation issues via property access.
|
|
@@ -56,43 +199,6 @@ interface FormButtonProps {
|
|
|
56
199
|
type: 'submit';
|
|
57
200
|
readonly formaction: string;
|
|
58
201
|
}
|
|
59
|
-
/** valid leaf value types for form fields */
|
|
60
|
-
type FormFieldValue = string | string[] | number | boolean | File | File[];
|
|
61
|
-
/** guard to prevent infinite recursion when T is unknown or has an index signature */
|
|
62
|
-
type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
|
|
63
|
-
/** base methods available on all form fields */
|
|
64
|
-
interface FormFieldMethods<T> {
|
|
65
|
-
/** get the current value */
|
|
66
|
-
value(): T | undefined;
|
|
67
|
-
/** set the value */
|
|
68
|
-
set(value: T): T;
|
|
69
|
-
/** get validation issues for this field */
|
|
70
|
-
issues(): FormIssue[] | undefined;
|
|
71
|
-
}
|
|
72
|
-
/** leaf field (primitives, files) with .as() method */
|
|
73
|
-
type FormFieldLeaf<T extends FormFieldValue> = FormFieldMethods<T> & {
|
|
74
|
-
/** get props for an input element */
|
|
75
|
-
as(type: string, value?: string): Record<string, unknown>;
|
|
76
|
-
};
|
|
77
|
-
/** container field (objects, arrays) with allIssues() method */
|
|
78
|
-
type FormFieldContainer<T> = FormFieldMethods<T> & {
|
|
79
|
-
/** get all issues for this field and descendants */
|
|
80
|
-
allIssues(): FormIssue[] | undefined;
|
|
81
|
-
};
|
|
82
|
-
/** fallback field type when recursion would be infinite */
|
|
83
|
-
type FormFieldUnknown<T> = FormFieldMethods<T> & {
|
|
84
|
-
/** get all issues for this field and descendants */
|
|
85
|
-
allIssues(): FormIssue[] | undefined;
|
|
86
|
-
/** get props for an input element */
|
|
87
|
-
as(type: string, value?: string): Record<string, unknown>;
|
|
88
|
-
} & {
|
|
89
|
-
[key: string | number]: FormFieldUnknown<unknown>;
|
|
90
|
-
};
|
|
91
|
-
/**
|
|
92
|
-
* recursive type to build form fields structure with proxy access.
|
|
93
|
-
* preserves type information through the object hierarchy.
|
|
94
|
-
*/
|
|
95
|
-
type FormFields<T> = T extends void ? Record<string, never> : WillRecurseIndefinitely<T> extends true ? FormFieldUnknown<T> : NonNullable<T> extends string | number | boolean | File ? FormFieldLeaf<NonNullable<T>> : T extends string[] | File[] ? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> } : T extends Array<infer U> ? FormFieldContainer<T> & { [K in number]: FormFields<U> } : FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };
|
|
96
202
|
/**
|
|
97
203
|
* creates a form without validation.
|
|
98
204
|
*/
|
|
@@ -138,7 +244,7 @@ type FormDefinitions = Record<string, Form<any, any>>;
|
|
|
138
244
|
* });
|
|
139
245
|
* ```
|
|
140
246
|
*/
|
|
141
|
-
declare function forms(definitions: FormDefinitions):
|
|
247
|
+
declare function forms(definitions: FormDefinitions): Middleware;
|
|
142
248
|
//#endregion
|
|
143
249
|
//#region src/lib/errors.d.ts
|
|
144
250
|
/**
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/lib/types.ts","../src/lib/form.ts","../src/lib/middleware.ts","../src/lib/errors.ts"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/lib/types.ts","../src/lib/form.ts","../src/lib/middleware.ts","../src/lib/errors.ts"],"sourcesContent":[],"mappings":";;;;KAEY,kBAAkB,IAAI,QAAQ;KAErC,gBAAgB,IAAI;;;AAFzB;AAA8B,UAWb,SAAA,CAXa;EAAY,CAAA,GAAA,EAAA,MAAA,CAAA,EAY1B,UAZ0B,CAAA,MAAA,GAAA,MAAA,GAAA,OAAA,GAYa,IAZb,GAYoB,SAZpB,CAAA;;;AAAG;AAW7C;AACuD,UAMtC,SAAA,CANsC;EAAO,OAAA,EAAA,MAAA;EAA9C,IAAA,EAAA,CAAA,MAAA,GAAA,MAAA,CAAA,EAAA;;;;;AA8EL,KA9CC,YAAA,GA8CD;EACR,IAAA,EAAA,MAAA;EACQ,KAAA,EAAA,MAAA;EAAa,QAAA,EAAA,MAAA;EACb,GAAA,EAAA,MAAA;EAAI,GAAA,EAAA,MAAA;EAML,MAAA,EAAA,MAAA;EAQA,MAAA,EAAA,MAAS;EAMT,KAAA,EAAA,MAAA;EAOA,IAAA,EAAA,MAAA;EAOA,gBAAA,EAAA,MAAmB;EAOnB,IAAA,EAAA,MAAS;EAMT,KAAA,EAAA,MAAA;EAUE,IAAA,EAAA,MAAA;EAAkC,KAAA,EAAA,MAAA;EAAgB,QAAA,EAAA,OAAA,GAAA,MAAA,EAAA;EACxC,KAAA,EAAA,MAAA;EAAnB,IAAA,EA1FI,IA0FJ;EACA,eAAA,EA1Fe,IA0Ff,EAAA;EACC,MAAA,EAAA,MAAA;EACA,MAAA,EAAA,MAAA;EACC,MAAA,EAAA,MAAA;EACA,KAAA,EAAA,MAAA;EACC,KAAA,EAAA,MAAA;EACA,MAAA,EAAA,MAAA;EACC,iBAAA,EAAA,MAAA,EAAA;CACA;AASP;AAAkF;AAGC;AAKzE,KAjGE,cAiGF,CAAA,CAAA,CAAA,GAAA,QAEE,MAlGC,YAkGD,GAlGgB,CAkGhB,SAlG0B,YAkG1B,CAlGuC,CAkGvC,CAAA,GAlG4C,CAkG5C,GAAA,KAAA,EAAI,CAAA,MAjGR,YAiGQ,CAAA;;;AAQhB;AAAoC,KApGxB,MAoGwB,CAAA,aAAA,MApGE,YAoGF,EAAA,KAAA,CAAA,GApGyB,IAoGzB,SAAA,UAAA,GAnGjC,KAmGiC,SAAA,MAAA,EAAA,GAAA,CAAA,IAAA,EAlGzB,IAkGyB,EAAA,KAAA,EAlGZ,KAkGY,CAAA,MAAA,CAAA,GAAA,CAAA,MAAA,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,IAAA,EAjGzB,IAiGyB,CAAA,GAhGjC,IAgGiC,SAAA,OAAA,GAAA,QAAA,GAAA,QAAA,GAAA,CAAA,IAAA,EA/FzB,IA+FyB,EAAA,KAAA,EA/FZ,KA+FY,GAAA,CAAA,MAAA,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,IAAA,EA9FzB,IA8FyB,CAAA;UAxF1B,kBAwFyD,CAAA,UAAA,UAAA,GAAA,OAAA,CAAA,CAAA;EAAb,IAAA,EAAA,MAAA;EAazB,IAAA,EAnGtB,CAmGsB;EAAf,KAAA,EAAA,MAAA;EAAmC,cAAA,CAAA,EAAA,MAAA;EAAG,SAAA,OAAA,EAAA,OAAA;;UA7F1C,SAAA,CA6FiE;EAAlB,IAAA,EAAA,MAAA;EAAiB,IAAA,EAAA,MAAA;EAM9D,cAAA,CAAA,EAAA,MAAkB;;UA7FpB,iBAAA,CA6F0B;EAEtB,IAAA,EAAA,MAAA;EAAS,IAAA,EAAA,MAAA;EAMlB,QAAA,EAAA,IAAA;EAAmC,cAAA,CAAA,EAAA,MAAA;;UA9F9B,WAAA,CAgGI;EAEe,IAAA,EAAA,MAAA;EAAf,QAAA,EAAA,KAAA;EAAgD,cAAA,CAAA,EAAA,MAAA;EAAG,SAAA,KAAA,EAAA,MAAA;;UA3FvD,mBAAA,CA2F2F;EAAlB,IAAA,EAAA,MAAA;EAE1D,QAAA,EAAA,IAAA;EAAgB,cAAA,CAAA,EAAA,MAAA;EAO7B,SAAA,KAAU,EAAA,MAAA,EAAA;;UA7FZ,SAAA,CA8FP;EACwB,IAAA,EAAA,MAAA;EAAxB,cAAA,CAAA,EAAA,MAAA;EACkB,SAAA,KAAA,EAAA,MAAA;;UA1FX,eA2FM,CAAA,UAAA,MAAA,CAAA,CAAA;EAAZ,IAAA,EAAA,MAAA;EAAmD,IAAA,EAzFhD,CAyFgD;EACxB,cAAA,CAAA,EAAA,MAAA;EAAZ,SAAA,KAAA,EAAA,MAAA;;;;;AAEb,KApFM,iBAoFN,CAAA,UAAA,MApFwC,YAoFxC,CAAA,GApFwD,CAoFxD,SAAA,UAAA,GAAA,OAAA,GAnFH,kBAmFG,CAnFgB,CAmFhB,CAAA,GAlFH,CAkFG,SAAA,MAAA,GAjFF,SAiFE,GAhFF,CAgFE,SAAA,eAAA,GA/ED,iBA+EC,GA9ED,CA8EC,SAAA,QAAA,GA7EA,WA6EA,GA5EA,CA4EA,SAAA,iBAAA,GA3EC,mBA2ED,GA1EC,CA0ED,SAAA,MAAA,GAzEE,SAyEF,GAxEE,eAwEF,CAxEkB,CAwElB,CAAA;;AAAoC,KAjE9B,cAAA,GAiE8B,MAAA,GAAA,MAAA,EAAA,GAAA,MAAA,GAAA,OAAA,GAjE0B,IAiE1B,GAjEiC,IAiEjC,EAAA;;KA9DrC,uBA+DW,CAAA,CAAA,CAAA,GAAA,OAAA,SA/DkC,CA+DlC,GAAA,IAAA,GAAA,MAAA,SAAA,MA/DkE,CA+DlE,GAAA,IAAA,GAAA,KAAA;;UA5DN,YA6DH,CAAA,CAAA,CAAA,CAAA;EAAoD;EAAX,KAAA,EAAA,EA3DtC,CA2DsC,GAAA,SAAA;EACtB;EAAnB,GAAA,CAAA,KAAA,EA1DK,CA0DL,CAAA,EA1DS,CA0DT;EAAsC;EAAiB,MAAA,EAAA,EAxDnD,SAwDmD,EAAA,GAAA,SAAA;;;;;KAlDlD,wBAAwB,kBAAkB,aAAa;;ACpKnE;;;;;;;;;;;EAIK,EAAA,CAAA,YD6KS,cC7KT,CD6KwB,CC7KxB,CAAA,CAAA,CAAA,GAAA,IAAA,ED6KqC,MC7KrC,CD6K4C,GC7K5C,ED6K+C,CC7K/C,CAAA,CAAA,ED6KoD,iBC7KpD,CD6KsE,GC7KtE,CAAA;CACqB;;AACxB;;AAGgD,KD8KtC,kBC9KsC,CAAA,CAAA,CAAA,GD8Kd,YC9Kc,CD8KD,CC9KC,CAAA,GAAA;EAAb;EAAuC,SAAA,EAAA,EDgL9D,SChL+E,EAAA,GAAA,SAAA;CACnE;;AAyD1B;;KD4HK,gBCtHa,CAAA,CAAA,CAAA,GDsHS,YCtHT,CDsHsB,CCtHtB,CAAA,GAAA;EAEW;EAAX,SAAA,EAAA,EDsHJ,SCtHI,EAAA,GAAA,SAAA;EAEK;EAAe,EAAA,CAAA,YDsHxB,cCtHwB,CDsHT,cCtHS,CAAA,CAAA,CAAA,GAAA,IAAA,EDsHiB,MCtHjB,CDsHwB,GCtHxB,EDsH2B,cCtH3B,CAAA,CAAA,EDsH6C,iBCtH7C,CDsH+D,GCtH/D,CAAA;AAYtC,CAAA,GAAiB;EA0GD,CAAA,GAAA,EAAA,MAAI,GAAA,MAAA,CAAA,EDEK,gBCFL,CAAA,OAAA,CAAA;CAAgC;;;;;AAKpC,KDIJ,UCJQ,CAAA,CAAA,CAAA,GDIQ,CCJR,SAAA,IAAA,GDKjB,MCLiB,CAAA,MAAA,EAAA,KAAA,CAAA,GDMjB,uBCNiB,CDMO,CCNP,CAAA,SAAA,IAAA,GDOhB,gBCPgB,CDOC,CCPD,CAAA,GDQhB,WCRgB,CDQJ,CCRI,CAAA,SAAA,MAAA,GAAA,MAAA,GAAA,OAAA,GDQmC,ICRnC,GDSf,aCTe,CDSD,WCTC,CDSW,CCTX,CAAA,CAAA,GDUf,CCVe,SAAA,MAAA,EAAA,GDUM,ICVN,EAAA,GDWd,aCXc,CDWA,CCXA,CAAA,GAAA,QAAe,MAAA,GDWO,aCXP,CDWqB,CCXrB,CAAA,MAAA,CAAA,CAAA,EAEvB,GDUN,CCVM,SDUI,KCVJ,CAAA,KAAA,EAAA,CAAA,GDWL,kBCXK,CDWc,CCXd,CAAA,GAAA,QAA2B,MAAA,GDWS,UCXT,CDWoB,CCXpB,CAAA,EAAb,GDYnB,kBCZmB,CDYA,CCZA,CAAA,GAAA,QAAqC,MDYlB,CCZkB,KDYZ,UCZY,CDYD,CCZC,CDYC,CCZD,CAAA,CAAA,EAAb;;;;;ADjOlD;;;;;AAA6C;AAW7C;;;;;AAOA;AA0BA;AAoCY,KCzDA,YDyDc,CAAA,CAAA,CAAA,GAAA,CAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GCzD0B,gBAAA,CAAiB,KDyD3C,CAAA,GAAA,QACb,MCzDA,CDyDA,KCzDM,CDyDN,CCzDQ,CDyDR,CAAA,SAAA,CAAA,KAAA,EAAA,CAAA,EAAA,GCxDT,iBDwDS,CCxDS,CDwDT,CAAA,GCvDT,CDuDS,CCvDP,CDuDO,CAAA,SAAA,MAAA,GCtDR,YDsDQ,CCtDK,CDsDL,CCtDO,CDsDP,CAAA,CAAA,GAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GCrDa,gBAAA,CAAiB,KDqD9B,EAAe;KClDvB,iBDkDiC,CAAA,CAAA,CAAA,GAAA;EAAa,CAAA,KAAA,EAAA,MAAA,CAAA,ECjDjC,CDiDiC,SAAA,MAAA,GCjDd,YDiDc,CCjDD,CDiDC,CAAA,GAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GCjDyB,gBAAA,CAAiB,KDiD1C;CAAK,GAAA,CAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GChD9B,gBAAA,CAAiB,KDgDa,CAAA;;;AAMxD;;;;AAoGA;;AAAmE,UCjGlD,IDiGkD,CAAA,cCjG/B,SDiG+B,GAAA,IAAA,EAAA,MAAA,CAAA,CAAA;EAAb;EAazB,SAAA,MAAA,EAAA,MAAA;EAAf;EAAmC,SAAA,MAAA,EAAA,MAAA;EAAG;EAAV,SAAA,MAAA,ECxGxB,MDwGwB,GAAA,SAAA;EAAiC;EAAlB,SAAA,MAAA,ECtGvC,UDsGuC,CCtG5B,KDsG4B,CAAA;EAAiB;EAM9D,SAAA,WAAA,EC1GW,eD0GO;;AAYhB,UC1GG,eAAA,CD0GH;EAAgD,IAAA,EAAA,QAAA;EAAG,SAAA,UAAA,EAAA,MAAA;;;;;AAc5D,iBCdW,IDcX,CAAA,MAAA,CAAA,CAAA,EAAA,EAAA,GAAA,GCdkC,YDclC,CCd+C,MDc/C,CAAA,CAAA,ECdyD,IDczD,CAAA,IAAA,ECdoE,MDcpE,CAAA;;;;AAEC,iBCXU,IDWV,CAAA,cCX6B,SDW7B,EAAA,MAAA,CAAA,CAAA,QAAA,EAAA,WAAA,EAAA,EAAA,EAAA,CAAA,IAAA,ECTM,KDSN,EAAA,KAAA,ECToB,YDSpB,CCTiC,KDSjC,CAAA,EAAA,GCT4C,YDS5C,CCTyD,MDSzD,CAAA,CAAA,ECRH,IDQG,CCRE,KDQF,ECRS,MDQT,CAAA;;;;AACU,iBCJA,IDIA,CAAA,eCJoB,gBDIpB,CCJqC,SDIrC,ECJgD,MDIhD,CAAA,MAAA,EAAA,OAAA,CAAA,CAAA,EAAA,MAAA,CAAA,CAAA,QAAA,ECHL,MDGK,EAAA,EAAA,EAAA,CAAA,IAAA,ECDR,gBAAA,CAAiB,WDCT,CCDqB,MDCrB,CAAA,EAAA,KAAA,ECAP,YDAO,CCAM,gBAAA,CAAiB,UDAvB,CCAkC,MDAlC,CAAA,CAAA,EAAA,GCCV,YDDU,CCCG,MDDH,CAAA,CAAA,ECEb,IDFa,CCER,gBAAA,CAAiB,UDFT,CCEoB,MDFpB,CAAA,ECE6B,MDF7B,CAAA;;;;;AA3OhB;AAA8B,KEiBlB,eAAA,GAAkB,MFjBA,CAAA,MAAA,EEiBe,IFjBf,CAAA,GAAA,EAAA,GAAA,CAAA,CAAA;;;;AAAe;AAW7C;;;;;AAOA;AA0BA;AAoCA;;;;;;;;AAOA;;;;;;;;AAKW,iBEtBK,KAAA,CFsBL,WAAA,EEtBwB,eFsBxB,CAAA,EEtB0C,UFsB1C;;;;;;AA5FC,cGGC,eAAA,SAAwB,KAAA,CHHb;EAAM,MAAA,EGIrB,gBAAA,CAAiB,KHJI,EAAA;EAAY,WAAA,CAAA,MAAA,EGMrB,gBAAA,CAAiB,KHNI,EAAA;;;AAAG;AAW7C;;;;;AAOA;AA0BA;AAoCA;;;;;;;;AAOA;;;;;AAEwB,iBGtDR,OAAA,CHsDQ,GAAA,MAAA,EAAA,CGtDY,gBAAA,CAAiB,KHsD7B,GAAA,MAAA,CAAA,EAAA,CAAA,EAAA,KAAA;;;;AAGA,iBGlDR,iBAAA,CHkDQ,CAAA,EAAA,OAAA,CAAA,EAAA,CAAA,IGlD4B,eHkD5B"}
|
package/dist/index.mjs
CHANGED
|
@@ -446,6 +446,14 @@ function isForm(value) {
|
|
|
446
446
|
return value !== null && typeof value === "object" && kForm in value;
|
|
447
447
|
}
|
|
448
448
|
/**
|
|
449
|
+
* checks if the request is a cross-origin request based on Sec-Fetch-Site header.
|
|
450
|
+
* used for CSRF protection.
|
|
451
|
+
*/
|
|
452
|
+
function isCrossOrigin(request) {
|
|
453
|
+
const secFetchSite = request.headers.get("sec-fetch-site");
|
|
454
|
+
return secFetchSite !== null && secFetchSite !== "same-origin" && secFetchSite !== "none";
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
449
457
|
* creates a forms middleware that registers forms and handles form submissions.
|
|
450
458
|
*
|
|
451
459
|
* @example
|
|
@@ -481,8 +489,7 @@ function forms(definitions) {
|
|
|
481
489
|
formConfig.set(f, { id: name });
|
|
482
490
|
formsById.set(name, f);
|
|
483
491
|
}
|
|
484
|
-
return async (
|
|
485
|
-
const { url, request, store } = context;
|
|
492
|
+
return async ({ request, url, store }, next) => {
|
|
486
493
|
const formStore = {
|
|
487
494
|
configs: formConfig,
|
|
488
495
|
state: /* @__PURE__ */ new WeakMap()
|
|
@@ -491,9 +498,12 @@ function forms(definitions) {
|
|
|
491
498
|
const action = url.searchParams.get("__action");
|
|
492
499
|
if (action && request.method === "POST") {
|
|
493
500
|
const formInstance = formsById.get(action);
|
|
494
|
-
if (formInstance)
|
|
501
|
+
if (formInstance) {
|
|
502
|
+
if (isCrossOrigin(request)) return new Response(null, { status: 403 });
|
|
503
|
+
setFormState(formInstance, await processForm(formInstance, convertFormData(await request.formData())));
|
|
504
|
+
}
|
|
495
505
|
}
|
|
496
|
-
return next(
|
|
506
|
+
return next();
|
|
497
507
|
};
|
|
498
508
|
}
|
|
499
509
|
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["result: Record<string, unknown>","values: unknown[]","current: Record<string, unknown>","current: unknown","normalized: InternalFormIssue","result: Record<string, InternalFormIssue[]>","target","baseProps: InputProps","form","schema: StandardSchemaV1 | null","info: FormInfo","result: Record<string, unknown>","issue","formStore: FormStore"],"sources":["../src/lib/errors.ts","../src/lib/form-utils.ts","../src/lib/form.ts","../src/lib/middleware.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec';\n\n/**\n * error thrown when form validation fails imperatively\n */\nexport class ValidationError extends Error {\n\tissues: StandardSchemaV1.Issue[];\n\n\tconstructor(issues: StandardSchemaV1.Issue[]) {\n\t\tsuper('Validation failed');\n\t\tthis.name = 'ValidationError';\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * use this to throw a validation error to imperatively fail form validation.\n * can be used in combination with `issue` passed to form actions to create field-specific issues.\n *\n * @example\n * ```ts\n * import { invalid, form } from '@oomfware/forms';\n * import * as v from 'valibot';\n *\n * export const login = form(\n * v.object({ name: v.string(), _password: v.string() }),\n * async ({ name, _password }, issue) => {\n * const success = tryLogin(name, _password);\n * if (!success) {\n * invalid('Incorrect username or password');\n * }\n *\n * // ...\n * }\n * );\n * ```\n */\nexport function invalid(...issues: (StandardSchemaV1.Issue | string)[]): never {\n\tthrow new ValidationError(issues.map((issue) => (typeof issue === 'string' ? { message: issue } : issue)));\n}\n\n/**\n * checks whether this is a validation error thrown by {@link invalid}.\n */\nexport function isValidationError(e: unknown): e is ValidationError {\n\treturn e instanceof ValidationError;\n}\n","import type { StandardSchemaV1 } from '@standard-schema/spec';\n\n/**\n * internal representation of a form validation issue with computed path info\n */\nexport interface InternalFormIssue {\n\t/** dot/bracket notation path string (e.g., \"user.emails[0]\") */\n\tname: string;\n\t/** path segments as array */\n\tpath: (string | number)[];\n\t/** error message */\n\tmessage: string;\n\t/** whether this issue came from server validation */\n\tserver: boolean;\n}\n\n/**\n * sets a value in a nested object using a path string, mutating the original object\n */\nexport function setNestedValue(object: Record<string, unknown>, pathString: string, value: unknown): void {\n\tif (pathString.startsWith('n:')) {\n\t\tpathString = pathString.slice(2);\n\t\tvalue = value === '' ? undefined : parseFloat(value as string);\n\t} else if (pathString.startsWith('b:')) {\n\t\tpathString = pathString.slice(2);\n\t\tvalue = value === 'on';\n\t}\n\n\tdeepSet(object, splitPath(pathString), value);\n}\n\n/**\n * convert `FormData` into a POJO\n */\nexport function convertFormData(data: FormData): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {};\n\n\tfor (let key of data.keys()) {\n\t\tconst isArray = key.endsWith('[]');\n\t\tlet values: unknown[] = data.getAll(key);\n\n\t\tif (isArray) {\n\t\t\tkey = key.slice(0, -2);\n\t\t}\n\n\t\tif (values.length > 1 && !isArray) {\n\t\t\tthrow new Error(`Form cannot contain duplicated keys — \"${key}\" has ${values.length} values`);\n\t\t}\n\n\t\t// an empty `<input type=\"file\">` will submit a non-existent file, bizarrely\n\t\tvalues = values.filter(\n\t\t\t(entry) => typeof entry === 'string' || (entry as File).name !== '' || (entry as File).size > 0,\n\t\t);\n\n\t\tif (key.startsWith('n:')) {\n\t\t\tkey = key.slice(2);\n\t\t\tvalues = values.map((v) => (v === '' ? undefined : parseFloat(v as string)));\n\t\t} else if (key.startsWith('b:')) {\n\t\t\tkey = key.slice(2);\n\t\t\tvalues = values.map((v) => v === 'on');\n\t\t}\n\n\t\tsetNestedValue(result, key, isArray ? values : values[0]);\n\t}\n\n\treturn result;\n}\n\nconst PATH_REGEX = /^[a-zA-Z_$]\\w*(\\.[a-zA-Z_$]\\w*|\\[\\d+\\])*$/;\n\n/**\n * splits a path string like \"user.emails[0].address\" into [\"user\", \"emails\", \"0\", \"address\"]\n */\nexport function splitPath(path: string): string[] {\n\tif (!PATH_REGEX.test(path)) {\n\t\tthrow new Error(`Invalid path ${path}`);\n\t}\n\n\treturn path.split(/\\.|\\[|\\]/).filter(Boolean);\n}\n\n/**\n * check if a property key is dangerous and could lead to prototype pollution\n */\nfunction checkPrototypePollution(key: string): void {\n\tif (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n\t\tthrow new Error(`Invalid key \"${key}\": This key is not allowed to prevent prototype pollution.`);\n\t}\n}\n\n/**\n * sets a value in a nested object using an array of keys, mutating the original object.\n */\nexport function deepSet(object: Record<string, unknown>, keys: string[], value: unknown): void {\n\tlet current: Record<string, unknown> = object;\n\n\tfor (let i = 0; i < keys.length - 1; i += 1) {\n\t\tconst key = keys[i]!;\n\n\t\tcheckPrototypePollution(key);\n\n\t\tconst isArray = /^\\d+$/.test(keys[i + 1]!);\n\t\tconst exists = key in current;\n\t\tconst inner = current[key];\n\n\t\tif (exists && isArray !== Array.isArray(inner)) {\n\t\t\tthrow new Error(`Invalid array key ${keys[i + 1]}`);\n\t\t}\n\n\t\tif (!exists) {\n\t\t\tcurrent[key] = isArray ? [] : {};\n\t\t}\n\n\t\tcurrent = current[key] as Record<string, unknown>;\n\t}\n\n\tconst finalKey = keys[keys.length - 1]!;\n\tcheckPrototypePollution(finalKey);\n\tcurrent[finalKey] = value;\n}\n\n/**\n * gets a nested value from an object using a path array\n */\nexport function deepGet(object: Record<string, unknown>, path: (string | number)[]): unknown {\n\tlet current: unknown = object;\n\tfor (const key of path) {\n\t\tif (current == null || typeof current !== 'object') {\n\t\t\treturn current;\n\t\t}\n\t\tcurrent = (current as Record<string | number, unknown>)[key];\n\t}\n\treturn current;\n}\n\n/**\n * normalizes a Standard Schema issue into our internal format\n */\nexport function normalizeIssue(issue: StandardSchemaV1.Issue, server = false): InternalFormIssue {\n\tconst normalized: InternalFormIssue = { name: '', path: [], message: issue.message, server };\n\n\tif (issue.path !== undefined) {\n\t\tlet name = '';\n\n\t\tfor (const segment of issue.path) {\n\t\t\tconst key = typeof segment === 'object' ? (segment.key as string | number) : segment;\n\n\t\t\tnormalized.path.push(key as string | number);\n\n\t\t\tif (typeof key === 'number') {\n\t\t\t\tname += `[${key}]`;\n\t\t\t} else if (typeof key === 'string') {\n\t\t\t\tname += name === '' ? key : '.' + key;\n\t\t\t}\n\t\t}\n\n\t\tnormalized.name = name;\n\t}\n\n\treturn normalized;\n}\n\n/**\n * flattens issues into a lookup object keyed by path\n * includes a special '$' key containing all issues\n */\nexport function flattenIssues(issues: InternalFormIssue[]): Record<string, InternalFormIssue[]> {\n\tconst result: Record<string, InternalFormIssue[]> = {};\n\n\tfor (const issue of issues) {\n\t\t(result.$ ??= []).push(issue);\n\n\t\tlet name = '';\n\n\t\tif (issue.path !== undefined) {\n\t\t\tfor (const key of issue.path) {\n\t\t\t\tif (typeof key === 'number') {\n\t\t\t\t\tname += `[${key}]`;\n\t\t\t\t} else if (typeof key === 'string') {\n\t\t\t\t\tname += name === '' ? key : '.' + key;\n\t\t\t\t}\n\n\t\t\t\t(result[name] ??= []).push(issue);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * builds a path string from an array of path segments\n */\nexport function buildPathString(path: (string | number)[]): string {\n\tlet result = '';\n\n\tfor (const segment of path) {\n\t\tif (typeof segment === 'number') {\n\t\t\tresult += `[${segment}]`;\n\t\t} else {\n\t\t\tresult += result === '' ? segment : '.' + segment;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n// #region field proxy\n\nexport interface FieldIssue {\n\tpath: (string | number)[];\n\tmessage: string;\n}\n\nexport interface FieldProxyMethods<T> {\n\t/** get the current value of this field */\n\tvalue(): T | undefined;\n\t/** set the value of this field */\n\tset(value: T): T;\n\t/** get validation issues for this exact field */\n\tissues(): FieldIssue[] | undefined;\n\t/** get all validation issues for this field and its descendants */\n\tallIssues(): FieldIssue[] | undefined;\n\t/**\n\t * get props for binding to an input element.\n\t * returns an object with `name`, `aria-invalid`, and type-specific props.\n\t */\n\tas(type: InputType, value?: string): InputProps;\n}\n\nexport type InputType =\n\t| 'text'\n\t| 'number'\n\t| 'range'\n\t| 'checkbox'\n\t| 'radio'\n\t| 'file'\n\t| 'file multiple'\n\t| 'select'\n\t| 'select multiple'\n\t| 'hidden'\n\t| 'submit'\n\t| 'email'\n\t| 'password'\n\t| 'tel'\n\t| 'url'\n\t| 'date'\n\t| 'time'\n\t| 'datetime-local'\n\t| 'month'\n\t| 'week'\n\t| 'color'\n\t| 'search';\n\nexport interface InputProps {\n\tname: string;\n\t'aria-invalid'?: 'true';\n\ttype?: string;\n\tvalue?: string;\n\tchecked?: boolean;\n\tmultiple?: boolean;\n}\n\n/**\n * creates a proxy-based field accessor for form data.\n * allows type-safe nested field access like `fields.user.emails[0].address.value()`.\n */\nexport function createFieldProxy<T>(\n\ttarget: unknown,\n\tgetInput: () => Record<string, unknown>,\n\tsetInput: (path: (string | number)[], value: unknown) => void,\n\tgetIssues: () => Record<string, InternalFormIssue[]>,\n\tpath: (string | number)[] = [],\n): T {\n\tconst getValue = () => {\n\t\treturn deepGet(getInput(), path);\n\t};\n\n\treturn new Proxy(target as object, {\n\t\tget(target, prop) {\n\t\t\tif (typeof prop === 'symbol') return (target as Record<symbol, unknown>)[prop];\n\n\t\t\t// Handle array access like jobs[0]\n\t\t\tif (/^\\d+$/.test(prop)) {\n\t\t\t\treturn createFieldProxy({}, getInput, setInput, getIssues, [...path, parseInt(prop, 10)]);\n\t\t\t}\n\n\t\t\tconst key = buildPathString(path);\n\n\t\t\tif (prop === 'set') {\n\t\t\t\tconst setFunc = function (newValue: unknown) {\n\t\t\t\t\tsetInput(path, newValue);\n\t\t\t\t\treturn newValue;\n\t\t\t\t};\n\t\t\t\treturn createFieldProxy(setFunc, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'value') {\n\t\t\t\treturn createFieldProxy(getValue, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'issues' || prop === 'allIssues') {\n\t\t\t\tconst issuesFunc = (): FieldIssue[] | undefined => {\n\t\t\t\t\tconst allIssues = getIssues()[key === '' ? '$' : key];\n\n\t\t\t\t\tif (prop === 'allIssues') {\n\t\t\t\t\t\treturn allIssues?.map((issue) => ({\n\t\t\t\t\t\t\tpath: issue.path,\n\t\t\t\t\t\t\tmessage: issue.message,\n\t\t\t\t\t\t}));\n\t\t\t\t\t}\n\n\t\t\t\t\treturn allIssues\n\t\t\t\t\t\t?.filter((issue) => issue.name === key)\n\t\t\t\t\t\t?.map((issue) => ({\n\t\t\t\t\t\t\tpath: issue.path,\n\t\t\t\t\t\t\tmessage: issue.message,\n\t\t\t\t\t\t}));\n\t\t\t\t};\n\n\t\t\t\treturn createFieldProxy(issuesFunc, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'as') {\n\t\t\t\tconst asFunc = (type: InputType, inputValue?: string): InputProps => {\n\t\t\t\t\tconst isArray =\n\t\t\t\t\t\ttype === 'file multiple' ||\n\t\t\t\t\t\ttype === 'select multiple' ||\n\t\t\t\t\t\t(type === 'checkbox' && typeof inputValue === 'string');\n\n\t\t\t\t\tconst prefix =\n\t\t\t\t\t\ttype === 'number' || type === 'range' ? 'n:' : type === 'checkbox' && !isArray ? 'b:' : '';\n\n\t\t\t\t\t// Base properties for all input types\n\t\t\t\t\tconst baseProps: InputProps = {\n\t\t\t\t\t\tname: prefix + key + (isArray ? '[]' : ''),\n\t\t\t\t\t\tget 'aria-invalid'() {\n\t\t\t\t\t\t\tconst issues = getIssues();\n\t\t\t\t\t\t\treturn key in issues ? 'true' : undefined;\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\n\t\t\t\t\t// Add type attribute only for non-text inputs and non-select elements\n\t\t\t\t\tif (type !== 'text' && type !== 'select' && type !== 'select multiple') {\n\t\t\t\t\t\tbaseProps.type = type === 'file multiple' ? 'file' : type;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle submit and hidden inputs\n\t\t\t\t\tif (type === 'submit' || type === 'hidden') {\n\t\t\t\t\t\tif (!inputValue) {\n\t\t\t\t\t\t\tthrow new Error(`\\`${type}\\` inputs must have a value`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tvalue: { value: inputValue, enumerable: true },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle select inputs\n\t\t\t\t\tif (type === 'select' || type === 'select multiple') {\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tmultiple: { value: isArray, enumerable: true },\n\t\t\t\t\t\t\tvalue: {\n\t\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\t\treturn getValue();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle checkbox inputs\n\t\t\t\t\tif (type === 'checkbox' || type === 'radio') {\n\t\t\t\t\t\tif (type === 'radio' && !inputValue) {\n\t\t\t\t\t\t\tthrow new Error('Radio inputs must have a value');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (type === 'checkbox' && isArray && !inputValue) {\n\t\t\t\t\t\t\tthrow new Error('Checkbox array inputs must have a value');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tvalue: { value: inputValue ?? 'on', enumerable: true },\n\t\t\t\t\t\t\tchecked: {\n\t\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\t\tconst value = getValue();\n\n\t\t\t\t\t\t\t\t\tif (type === 'radio') {\n\t\t\t\t\t\t\t\t\t\treturn value === inputValue;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tif (isArray) {\n\t\t\t\t\t\t\t\t\t\treturn ((value as string[] | undefined) ?? []).includes(inputValue!);\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle file inputs (can't persist value, just return name/type/multiple)\n\t\t\t\t\tif (type === 'file' || type === 'file multiple') {\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tmultiple: { value: isArray, enumerable: true },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle all other input types (text, number, etc.)\n\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\tvalue: {\n\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\tconst value = getValue();\n\t\t\t\t\t\t\t\treturn value != null ? String(value) : '';\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t};\n\n\t\t\t\treturn createFieldProxy(asFunc, getInput, setInput, getIssues, [...path, 'as']);\n\t\t\t}\n\n\t\t\t// Handle property access (nested fields)\n\t\t\treturn createFieldProxy({}, getInput, setInput, getIssues, [...path, prop]);\n\t\t},\n\t}) as T;\n}\n\n// #endregion\n","import { createInjectionKey } from '@oomfware/fetch-router';\nimport { getContext } from '@oomfware/fetch-router/middlewares/async-context';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\n\nimport { ValidationError } from './errors.ts';\nimport {\n\tcreateFieldProxy,\n\tdeepSet,\n\tflattenIssues,\n\tnormalizeIssue,\n\ttype InternalFormIssue,\n} from './form-utils.ts';\nimport type { MaybePromise } from './types.ts';\n\n// #region types\n\nexport interface FormInput {\n\t[key: string]: MaybeArray<string | number | boolean | File | FormInput>;\n}\n\ntype MaybeArray<T> = T | T[];\n\nexport interface FormIssue {\n\tmessage: string;\n\tpath: (string | number)[];\n}\n\n/**\n * the issue creator proxy passed to form callbacks.\n * allows creating field-specific validation issues via property access.\n *\n * @example\n * ```ts\n * form(schema, async (data, issue) => {\n * if (emailTaken(data.email)) {\n * invalid(issue.email('Email already in use'));\n * }\n * // nested fields: issue.user.profile.name('Invalid name')\n * // array fields: issue.items[0].name('Invalid item name')\n * });\n * ```\n */\nexport type InvalidField<T> = ((message: string) => StandardSchemaV1.Issue) & {\n\t[K in keyof T]-?: T[K] extends (infer U)[]\n\t\t? InvalidFieldArray<U>\n\t\t: T[K] extends object\n\t\t\t? InvalidField<T[K]>\n\t\t\t: (message: string) => StandardSchemaV1.Issue;\n};\n\ntype InvalidFieldArray<T> = {\n\t[index: number]: T extends object ? InvalidField<T> : (message: string) => StandardSchemaV1.Issue;\n} & ((message: string) => StandardSchemaV1.Issue);\n\n/**\n * symbol used to identify form instances.\n */\nexport const kForm = Symbol.for('@oomfware/forms');\n\n/**\n * internal info attached to a form instance.\n * used by the forms() middleware to identify and process forms.\n */\nexport interface FormInfo {\n\t/** the schema, if any */\n\tschema: StandardSchemaV1 | null;\n\t/** the handler function */\n\tfn: (data: any, issue: any) => MaybePromise<any>;\n}\n\n/**\n * form config stored by the forms() middleware.\n */\nexport interface FormConfig {\n\t/** the form id, derived from registration name */\n\tid: string;\n}\n\n/**\n * form state stored by the forms() middleware.\n */\nexport interface FormState<Input = unknown, Output = unknown> {\n\t/** the submitted input data (for repopulating form on error) */\n\tinput?: Input;\n\t/** validation issues, flattened by path */\n\tissues?: Record<string, InternalFormIssue[]>;\n\t/** the handler result (if successful) */\n\tresult?: Output;\n}\n\n/**\n * the form store holds registered forms, their configs, and state.\n */\nexport interface FormStore {\n\t/** map of form instance to config */\n\tconfigs: WeakMap<InternalForm<any, any>, FormConfig>;\n\t/** state for each form instance */\n\tstate: WeakMap<InternalForm<any, any>, FormState>;\n}\n\n/**\n * injection key for the form store.\n */\nexport const FORM_STORE_KEY = createInjectionKey<FormStore>();\n\n/**\n * the return value of a form() function.\n * can be spread onto a <form> element.\n */\nexport interface Form<Input extends FormInput | void, Output> {\n\t/** HTTP method */\n\treadonly method: 'POST';\n\t/** the form action URL */\n\treadonly action: string;\n\t/** the handler result, if submission was successful */\n\treadonly result: Output | undefined;\n\t/** access form fields using object notation */\n\treadonly fields: FormFields<Input>;\n\t/** spread this onto a <button> or <input type=\"submit\"> */\n\treadonly buttonProps: FormButtonProps;\n}\n\n/**\n * internal form type with metadata.\n * used internally by middleware; cast Form to this when accessing `__`.\n */\nexport interface InternalForm<Input extends FormInput | void, Output> extends Form<Input, Output> {\n\t/** internal form info, used by forms() middleware */\n\treadonly __: FormInfo;\n}\n\nexport interface FormButtonProps {\n\ttype: 'submit';\n\treadonly formaction: string;\n}\n\n// #region field types\n\n/** valid leaf value types for form fields */\nexport type FormFieldValue = string | string[] | number | boolean | File | File[];\n\n/** guard to prevent infinite recursion when T is unknown or has an index signature */\ntype WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;\n\n/** base methods available on all form fields */\nexport interface FormFieldMethods<T> {\n\t/** get the current value */\n\tvalue(): T | undefined;\n\t/** set the value */\n\tset(value: T): T;\n\t/** get validation issues for this field */\n\tissues(): FormIssue[] | undefined;\n}\n\n/** leaf field (primitives, files) with .as() method */\nexport type FormFieldLeaf<T extends FormFieldValue> = FormFieldMethods<T> & {\n\t/** get props for an input element */\n\tas(type: string, value?: string): Record<string, unknown>;\n};\n\n/** container field (objects, arrays) with allIssues() method */\ntype FormFieldContainer<T> = FormFieldMethods<T> & {\n\t/** get all issues for this field and descendants */\n\tallIssues(): FormIssue[] | undefined;\n};\n\n/** fallback field type when recursion would be infinite */\ntype FormFieldUnknown<T> = FormFieldMethods<T> & {\n\t/** get all issues for this field and descendants */\n\tallIssues(): FormIssue[] | undefined;\n\t/** get props for an input element */\n\tas(type: string, value?: string): Record<string, unknown>;\n} & {\n\t[key: string | number]: FormFieldUnknown<unknown>;\n};\n\n/**\n * recursive type to build form fields structure with proxy access.\n * preserves type information through the object hierarchy.\n */\nexport type FormFields<T> = T extends void\n\t? Record<string, never>\n\t: WillRecurseIndefinitely<T> extends true\n\t\t? FormFieldUnknown<T>\n\t\t: NonNullable<T> extends string | number | boolean | File\n\t\t\t? FormFieldLeaf<NonNullable<T>>\n\t\t\t: T extends string[] | File[]\n\t\t\t\t? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> }\n\t\t\t\t: T extends Array<infer U>\n\t\t\t\t\t? FormFieldContainer<T> & { [K in number]: FormFields<U> }\n\t\t\t\t\t: FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };\n\n// #endregion\n\n// #region issue creator\n\n/**\n * creates an issue creator proxy that builds up paths for field-specific issues.\n */\nfunction createIssueCreator<T>(): InvalidField<T> {\n\treturn new Proxy((message: string) => createIssue(message), {\n\t\tget(_target, prop) {\n\t\t\tif (typeof prop === 'symbol') return undefined;\n\t\t\treturn createIssueProxy(prop, []);\n\t\t},\n\t}) as InvalidField<T>;\n\n\tfunction createIssue(message: string, path: (string | number)[] = []): StandardSchemaV1.Issue {\n\t\treturn { message, path };\n\t}\n\n\tfunction createIssueProxy(\n\t\tkey: string | number,\n\t\tpath: (string | number)[],\n\t): (message: string) => StandardSchemaV1.Issue {\n\t\tconst newPath = [...path, key];\n\n\t\tconst issueFunc = (message: string) => createIssue(message, newPath);\n\n\t\treturn new Proxy(issueFunc, {\n\t\t\tget(_target, prop) {\n\t\t\t\tif (typeof prop === 'symbol') return undefined;\n\n\t\t\t\tif (/^\\d+$/.test(prop)) {\n\t\t\t\t\treturn createIssueProxy(parseInt(prop, 10), newPath);\n\t\t\t\t}\n\n\t\t\t\treturn createIssueProxy(prop, newPath);\n\t\t\t},\n\t\t});\n\t}\n}\n\n// #endregion\n\n// #region form state access\n\n/**\n * get the form store from the current request context.\n * @throws if called outside of a request context\n */\nexport function getFormStore(): FormStore {\n\tconst context = getContext();\n\tconst store = context.store.inject(FORM_STORE_KEY);\n\n\tif (!store) {\n\t\tthrow new Error('form store not found. make sure the forms() middleware is installed.');\n\t}\n\n\treturn store;\n}\n\n/**\n * get config for a specific form instance.\n * @throws if form is not registered with forms() middleware\n */\nfunction getFormConfig(form: InternalForm<any, any>): FormConfig {\n\tconst store = getFormStore();\n\tconst config = store.configs.get(form);\n\n\tif (!config) {\n\t\tthrow new Error('form not registered. make sure to pass it to the forms() middleware.');\n\t}\n\n\treturn config;\n}\n\n/**\n * get state for a specific form instance.\n */\nexport function getFormState<Input, Output>(\n\tform: InternalForm<any, any>,\n): FormState<Input, Output> | undefined {\n\tconst store = getFormStore();\n\treturn store.state.get(form) as FormState<Input, Output> | undefined;\n}\n\n/**\n * set state for a specific form instance.\n */\nexport function setFormState<Input, Output>(\n\tform: InternalForm<any, any>,\n\tstate: FormState<Input, Output>,\n): void {\n\tconst store = getFormStore();\n\tstore.state.set(form, state);\n}\n\n// #endregion\n\n// #region form function\n\n/**\n * creates a form without validation.\n */\nexport function form<Output>(fn: () => MaybePromise<Output>): Form<void, Output>;\n\n/**\n * creates a form with unchecked input (no validation).\n */\nexport function form<Input extends FormInput, Output>(\n\tvalidate: 'unchecked',\n\tfn: (data: Input, issue: InvalidField<Input>) => MaybePromise<Output>,\n): Form<Input, Output>;\n\n/**\n * creates a form with Standard Schema validation.\n */\nexport function form<Schema extends StandardSchemaV1<FormInput, Record<string, unknown>>, Output>(\n\tvalidate: Schema,\n\tfn: (\n\t\tdata: StandardSchemaV1.InferOutput<Schema>,\n\t\tissue: InvalidField<StandardSchemaV1.InferInput<Schema>>,\n\t) => MaybePromise<Output>,\n): Form<StandardSchemaV1.InferInput<Schema>, Output>;\n\nexport function form(\n\tvalidateOrFn: StandardSchemaV1 | 'unchecked' | (() => MaybePromise<unknown>),\n\tmaybeFn?: (data: any, issue: any) => MaybePromise<unknown>,\n): Form<any, any> {\n\tconst fn = (maybeFn ?? validateOrFn) as (data: any, issue: any) => MaybePromise<unknown>;\n\n\tconst schema: StandardSchemaV1 | null =\n\t\t!maybeFn || validateOrFn === 'unchecked' ? null : (validateOrFn as StandardSchemaV1);\n\n\tconst instance = {} as InternalForm<any, any>;\n\n\tconst info: FormInfo = {\n\t\tschema,\n\t\tfn,\n\t};\n\n\t// method\n\tObject.defineProperty(instance, 'method', {\n\t\tvalue: 'POST',\n\t\tenumerable: true,\n\t});\n\n\t// action - computed from form store\n\tObject.defineProperty(instance, 'action', {\n\t\tget() {\n\t\t\tconst config = getFormConfig(instance);\n\t\t\treturn `?__action=${config.id}`;\n\t\t},\n\t\tenumerable: true,\n\t});\n\n\t// result - from state store\n\tObject.defineProperty(instance, 'result', {\n\t\tget() {\n\t\t\treturn getFormState(instance)?.result;\n\t\t},\n\t});\n\n\t// fields - proxy for field access\n\tObject.defineProperty(instance, 'fields', {\n\t\tget() {\n\t\t\treturn createFieldProxy(\n\t\t\t\t{},\n\t\t\t\t() => (getFormState(instance)?.input as Record<string, unknown>) ?? {},\n\t\t\t\t(path, value) => {\n\t\t\t\t\tconst currentState = getFormState(instance) ?? { input: {} };\n\t\t\t\t\tif (path.length === 0) {\n\t\t\t\t\t\tsetFormState(instance, { ...currentState, input: value });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst input = (currentState.input as Record<string, unknown>) ?? {};\n\t\t\t\t\t\tdeepSet(input, path.map(String), value);\n\t\t\t\t\t\tsetFormState(instance, { ...currentState, input });\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => getFormState(instance)?.issues ?? {},\n\t\t\t);\n\t\t},\n\t});\n\n\t// buttonProps\n\tObject.defineProperty(instance, 'buttonProps', {\n\t\tget() {\n\t\t\tconst config = getFormConfig(instance);\n\t\t\treturn {\n\t\t\t\ttype: 'submit' as const,\n\t\t\t\tformaction: `?__action=${config.id}`,\n\t\t\t};\n\t\t},\n\t});\n\n\t// internal info\n\tObject.defineProperty(instance, '__', {\n\t\tvalue: info,\n\t});\n\n\t// brand symbol for identification\n\tObject.defineProperty(instance, kForm, {\n\t\tvalue: true,\n\t\tenumerable: false,\n\t});\n\n\treturn instance;\n}\n\n// #endregion\n\n// #region form processing\n\n/**\n * redacts sensitive fields (those starting with `_`) from form input.\n * this prevents passwords and other sensitive data from being returned in form state.\n */\nfunction redactSensitiveFields(obj: Record<string, unknown>): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {};\n\n\tfor (const key of Object.keys(obj)) {\n\t\tif (key.startsWith('_')) continue;\n\n\t\tconst value = obj[key];\n\n\t\tif (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof File)) {\n\t\t\tresult[key] = redactSensitiveFields(value as Record<string, unknown>);\n\t\t} else if (Array.isArray(value)) {\n\t\t\tresult[key] = value.map((item) =>\n\t\t\t\titem !== null && typeof item === 'object' && !(item instanceof File)\n\t\t\t\t\t? redactSensitiveFields(item as Record<string, unknown>)\n\t\t\t\t\t: item,\n\t\t\t);\n\t\t} else {\n\t\t\tresult[key] = value;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * process a form submission.\n * called by forms() middleware when a matching action is received.\n */\nexport async function processForm(formInstance: InternalForm<any, any>, data: FormInput): Promise<FormState> {\n\tconst { schema, fn } = formInstance.__;\n\n\tlet validatedData = data;\n\n\t// validate with schema if present\n\tif (schema) {\n\t\tconst result = await schema['~standard'].validate(data);\n\n\t\tif (result.issues) {\n\t\t\treturn {\n\t\t\t\tresult: undefined,\n\t\t\t\tissues: flattenIssues(result.issues.map((issue) => normalizeIssue(issue, true))),\n\t\t\t\tinput: redactSensitiveFields(data),\n\t\t\t};\n\t\t}\n\t\tvalidatedData = result.value as FormInput;\n\t}\n\n\t// run handler\n\tconst issue = createIssueCreator();\n\n\ttry {\n\t\treturn {\n\t\t\tresult: await fn(validatedData, issue),\n\t\t\tissues: undefined,\n\t\t\tinput: undefined,\n\t\t};\n\t} catch (e) {\n\t\tif (e instanceof ValidationError) {\n\t\t\treturn {\n\t\t\t\tresult: undefined,\n\t\t\t\tissues: flattenIssues(e.issues.map((issue) => normalizeIssue(issue, true))),\n\t\t\t\tinput: redactSensitiveFields(data),\n\t\t\t};\n\t\t}\n\n\t\tthrow e;\n\t}\n}\n\n// #endregion\n","import type { RouterMiddleware } from '@oomfware/fetch-router';\n\nimport { convertFormData } from './form-utils.ts';\nimport {\n\tFORM_STORE_KEY,\n\tkForm,\n\tprocessForm,\n\tsetFormState,\n\ttype Form,\n\ttype FormConfig,\n\ttype FormStore,\n\ttype InternalForm,\n} from './form.ts';\n\n// #region types\n\n/**\n * a record of form instances to register with the middleware.\n */\nexport type FormDefinitions = Record<string, Form<any, any>>;\n\n// #endregion\n\n// #region helpers\n\n/**\n * checks if a value is a form instance created by form().\n */\nfunction isForm(value: unknown): value is Form<any, any> {\n\treturn value !== null && typeof value === 'object' && kForm in value;\n}\n\n// #endregion\n\n// #region middleware\n\n/**\n * creates a forms middleware that registers forms and handles form submissions.\n *\n * @example\n * ```ts\n * import { form, forms } from '@oomfware/forms';\n * import * as v from 'valibot';\n *\n * const createUserForm = form(\n * v.object({ name: v.string(), password: v.string() }),\n * async (input, issue) => {\n * // handle form submission\n * },\n * );\n *\n * router.map(routes.admin, {\n * middleware: [forms({ createUserForm })],\n * action() {\n * return render(\n * <form {...createUserForm}>\n * <input {...createUserForm.fields.name.as('text')} required />\n * </form>\n * );\n * },\n * });\n * ```\n */\nexport function forms(definitions: FormDefinitions): RouterMiddleware {\n\tconst formConfig = new WeakMap<InternalForm<any, any>, FormConfig>();\n\tconst formsById = new Map<string, InternalForm<any, any>>();\n\n\tfor (const [name, formInstance] of Object.entries(definitions)) {\n\t\tif (!isForm(formInstance)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst f = formInstance as InternalForm<any, any>;\n\n\t\tformConfig.set(f, { id: name });\n\t\tformsById.set(name, f);\n\t}\n\n\treturn async (context, next) => {\n\t\tconst { url, request, store } = context;\n\n\t\t// create form store for this request\n\t\tconst formStore: FormStore = {\n\t\t\tconfigs: formConfig,\n\t\t\tstate: new WeakMap(),\n\t\t};\n\n\t\t// inject form store into context\n\t\tstore.provide(FORM_STORE_KEY, formStore);\n\n\t\t// check if this is a form submission\n\t\tconst action = url.searchParams.get('__action');\n\n\t\tif (action && request.method === 'POST') {\n\t\t\t// find the form\n\t\t\tconst formInstance = formsById.get(action);\n\n\t\t\tif (formInstance) {\n\t\t\t\t// parse form data\n\t\t\t\tconst formData = await request.formData();\n\t\t\t\tconst data = convertFormData(formData as unknown as FormData);\n\n\t\t\t\t// process the form\n\t\t\t\tconst state = await processForm(formInstance, data as any);\n\n\t\t\t\t// store the state\n\t\t\t\tsetFormState(formInstance, state);\n\t\t\t}\n\t\t}\n\n\t\treturn next(context);\n\t};\n}\n\n// #endregion\n"],"mappings":";;;;;;;AAKA,IAAa,kBAAb,cAAqC,MAAM;CAC1C;CAEA,YAAY,QAAkC;AAC7C,QAAM,oBAAoB;AAC1B,OAAK,OAAO;AACZ,OAAK,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;AA0BhB,SAAgB,QAAQ,GAAG,QAAoD;AAC9E,OAAM,IAAI,gBAAgB,OAAO,KAAK,UAAW,OAAO,UAAU,WAAW,EAAE,SAAS,OAAO,GAAG,MAAO,CAAC;;;;;AAM3G,SAAgB,kBAAkB,GAAkC;AACnE,QAAO,aAAa;;;;;;;;AC1BrB,SAAgB,eAAe,QAAiC,YAAoB,OAAsB;AACzG,KAAI,WAAW,WAAW,KAAK,EAAE;AAChC,eAAa,WAAW,MAAM,EAAE;AAChC,UAAQ,UAAU,KAAK,SAAY,WAAW,MAAgB;YACpD,WAAW,WAAW,KAAK,EAAE;AACvC,eAAa,WAAW,MAAM,EAAE;AAChC,UAAQ,UAAU;;AAGnB,SAAQ,QAAQ,UAAU,WAAW,EAAE,MAAM;;;;;AAM9C,SAAgB,gBAAgB,MAAyC;CACxE,MAAMA,SAAkC,EAAE;AAE1C,MAAK,IAAI,OAAO,KAAK,MAAM,EAAE;EAC5B,MAAM,UAAU,IAAI,SAAS,KAAK;EAClC,IAAIC,SAAoB,KAAK,OAAO,IAAI;AAExC,MAAI,QACH,OAAM,IAAI,MAAM,GAAG,GAAG;AAGvB,MAAI,OAAO,SAAS,KAAK,CAAC,QACzB,OAAM,IAAI,MAAM,0CAA0C,IAAI,QAAQ,OAAO,OAAO,SAAS;AAI9F,WAAS,OAAO,QACd,UAAU,OAAO,UAAU,YAAa,MAAe,SAAS,MAAO,MAAe,OAAO,EAC9F;AAED,MAAI,IAAI,WAAW,KAAK,EAAE;AACzB,SAAM,IAAI,MAAM,EAAE;AAClB,YAAS,OAAO,KAAK,MAAO,MAAM,KAAK,SAAY,WAAW,EAAY,CAAE;aAClE,IAAI,WAAW,KAAK,EAAE;AAChC,SAAM,IAAI,MAAM,EAAE;AAClB,YAAS,OAAO,KAAK,MAAM,MAAM,KAAK;;AAGvC,iBAAe,QAAQ,KAAK,UAAU,SAAS,OAAO,GAAG;;AAG1D,QAAO;;AAGR,MAAM,aAAa;;;;AAKnB,SAAgB,UAAU,MAAwB;AACjD,KAAI,CAAC,WAAW,KAAK,KAAK,CACzB,OAAM,IAAI,MAAM,gBAAgB,OAAO;AAGxC,QAAO,KAAK,MAAM,WAAW,CAAC,OAAO,QAAQ;;;;;AAM9C,SAAS,wBAAwB,KAAmB;AACnD,KAAI,QAAQ,eAAe,QAAQ,iBAAiB,QAAQ,YAC3D,OAAM,IAAI,MAAM,gBAAgB,IAAI,4DAA4D;;;;;AAOlG,SAAgB,QAAQ,QAAiC,MAAgB,OAAsB;CAC9F,IAAIC,UAAmC;AAEvC,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG;EAC5C,MAAM,MAAM,KAAK;AAEjB,0BAAwB,IAAI;EAE5B,MAAM,UAAU,QAAQ,KAAK,KAAK,IAAI,GAAI;EAC1C,MAAM,SAAS,OAAO;EACtB,MAAM,QAAQ,QAAQ;AAEtB,MAAI,UAAU,YAAY,MAAM,QAAQ,MAAM,CAC7C,OAAM,IAAI,MAAM,qBAAqB,KAAK,IAAI,KAAK;AAGpD,MAAI,CAAC,OACJ,SAAQ,OAAO,UAAU,EAAE,GAAG,EAAE;AAGjC,YAAU,QAAQ;;CAGnB,MAAM,WAAW,KAAK,KAAK,SAAS;AACpC,yBAAwB,SAAS;AACjC,SAAQ,YAAY;;;;;AAMrB,SAAgB,QAAQ,QAAiC,MAAoC;CAC5F,IAAIC,UAAmB;AACvB,MAAK,MAAM,OAAO,MAAM;AACvB,MAAI,WAAW,QAAQ,OAAO,YAAY,SACzC,QAAO;AAER,YAAW,QAA6C;;AAEzD,QAAO;;;;;AAMR,SAAgB,eAAe,OAA+B,SAAS,OAA0B;CAChG,MAAMC,aAAgC;EAAE,MAAM;EAAI,MAAM,EAAE;EAAE,SAAS,MAAM;EAAS;EAAQ;AAE5F,KAAI,MAAM,SAAS,QAAW;EAC7B,IAAI,OAAO;AAEX,OAAK,MAAM,WAAW,MAAM,MAAM;GACjC,MAAM,MAAM,OAAO,YAAY,WAAY,QAAQ,MAA0B;AAE7E,cAAW,KAAK,KAAK,IAAuB;AAE5C,OAAI,OAAO,QAAQ,SAClB,SAAQ,IAAI,IAAI;YACN,OAAO,QAAQ,SACzB,SAAQ,SAAS,KAAK,MAAM,MAAM;;AAIpC,aAAW,OAAO;;AAGnB,QAAO;;;;;;AAOR,SAAgB,cAAc,QAAkE;CAC/F,MAAMC,SAA8C,EAAE;AAEtD,MAAK,MAAM,SAAS,QAAQ;AAC3B,GAAC,OAAO,MAAM,EAAE,EAAE,KAAK,MAAM;EAE7B,IAAI,OAAO;AAEX,MAAI,MAAM,SAAS,OAClB,MAAK,MAAM,OAAO,MAAM,MAAM;AAC7B,OAAI,OAAO,QAAQ,SAClB,SAAQ,IAAI,IAAI;YACN,OAAO,QAAQ,SACzB,SAAQ,SAAS,KAAK,MAAM,MAAM;AAGnC,IAAC,OAAO,UAAU,EAAE,EAAE,KAAK,MAAM;;;AAKpC,QAAO;;;;;AAMR,SAAgB,gBAAgB,MAAmC;CAClE,IAAI,SAAS;AAEb,MAAK,MAAM,WAAW,KACrB,KAAI,OAAO,YAAY,SACtB,WAAU,IAAI,QAAQ;KAEtB,WAAU,WAAW,KAAK,UAAU,MAAM;AAI5C,QAAO;;;;;;AA+DR,SAAgB,iBACf,QACA,UACA,UACA,WACA,OAA4B,EAAE,EAC1B;CACJ,MAAM,iBAAiB;AACtB,SAAO,QAAQ,UAAU,EAAE,KAAK;;AAGjC,QAAO,IAAI,MAAM,QAAkB,EAClC,IAAI,UAAQ,MAAM;AACjB,MAAI,OAAO,SAAS,SAAU,QAAQC,SAAmC;AAGzE,MAAI,QAAQ,KAAK,KAAK,CACrB,QAAO,iBAAiB,EAAE,EAAE,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,SAAS,MAAM,GAAG,CAAC,CAAC;EAG1F,MAAM,MAAM,gBAAgB,KAAK;AAEjC,MAAI,SAAS,OAAO;GACnB,MAAM,UAAU,SAAU,UAAmB;AAC5C,aAAS,MAAM,SAAS;AACxB,WAAO;;AAER,UAAO,iBAAiB,SAAS,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;;AAGjF,MAAI,SAAS,QACZ,QAAO,iBAAiB,UAAU,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;AAGlF,MAAI,SAAS,YAAY,SAAS,aAAa;GAC9C,MAAM,mBAA6C;IAClD,MAAM,YAAY,WAAW,CAAC,QAAQ,KAAK,MAAM;AAEjD,QAAI,SAAS,YACZ,QAAO,WAAW,KAAK,WAAW;KACjC,MAAM,MAAM;KACZ,SAAS,MAAM;KACf,EAAE;AAGJ,WAAO,WACJ,QAAQ,UAAU,MAAM,SAAS,IAAI,EACrC,KAAK,WAAW;KACjB,MAAM,MAAM;KACZ,SAAS,MAAM;KACf,EAAE;;AAGL,UAAO,iBAAiB,YAAY,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;;AAGpF,MAAI,SAAS,MAAM;GAClB,MAAM,UAAU,MAAiB,eAAoC;IACpE,MAAM,UACL,SAAS,mBACT,SAAS,qBACR,SAAS,cAAc,OAAO,eAAe;IAM/C,MAAMC,YAAwB;KAC7B,OAJA,SAAS,YAAY,SAAS,UAAU,OAAO,SAAS,cAAc,CAAC,UAAU,OAAO,MAIzE,OAAO,UAAU,OAAO;KACvC,IAAI,iBAAiB;AAEpB,aAAO,OADQ,WAAW,GACH,SAAS;;KAEjC;AAGD,QAAI,SAAS,UAAU,SAAS,YAAY,SAAS,kBACpD,WAAU,OAAO,SAAS,kBAAkB,SAAS;AAItD,QAAI,SAAS,YAAY,SAAS,UAAU;AAC3C,SAAI,CAAC,WACJ,OAAM,IAAI,MAAM,KAAK,KAAK,6BAA6B;AAGxD,YAAO,OAAO,iBAAiB,WAAW,EACzC,OAAO;MAAE,OAAO;MAAY,YAAY;MAAM,EAC9C,CAAC;;AAIH,QAAI,SAAS,YAAY,SAAS,kBACjC,QAAO,OAAO,iBAAiB,WAAW;KACzC,UAAU;MAAE,OAAO;MAAS,YAAY;MAAM;KAC9C,OAAO;MACN,YAAY;MACZ,MAAM;AACL,cAAO,UAAU;;MAElB;KACD,CAAC;AAIH,QAAI,SAAS,cAAc,SAAS,SAAS;AAC5C,SAAI,SAAS,WAAW,CAAC,WACxB,OAAM,IAAI,MAAM,iCAAiC;AAGlD,SAAI,SAAS,cAAc,WAAW,CAAC,WACtC,OAAM,IAAI,MAAM,0CAA0C;AAG3D,YAAO,OAAO,iBAAiB,WAAW;MACzC,OAAO;OAAE,OAAO,cAAc;OAAM,YAAY;OAAM;MACtD,SAAS;OACR,YAAY;OACZ,MAAM;QACL,MAAM,QAAQ,UAAU;AAExB,YAAI,SAAS,QACZ,QAAO,UAAU;AAGlB,YAAI,QACH,SAAS,SAAkC,EAAE,EAAE,SAAS,WAAY;AAGrE,eAAO;;OAER;MACD,CAAC;;AAIH,QAAI,SAAS,UAAU,SAAS,gBAC/B,QAAO,OAAO,iBAAiB,WAAW,EACzC,UAAU;KAAE,OAAO;KAAS,YAAY;KAAM,EAC9C,CAAC;AAIH,WAAO,OAAO,iBAAiB,WAAW,EACzC,OAAO;KACN,YAAY;KACZ,MAAM;MACL,MAAM,QAAQ,UAAU;AACxB,aAAO,SAAS,OAAO,OAAO,MAAM,GAAG;;KAExC,EACD,CAAC;;AAGH,UAAO,iBAAiB,QAAQ,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;;AAIhF,SAAO,iBAAiB,EAAE,EAAE,UAAU,UAAU,WAAW,CAAC,GAAG,MAAM,KAAK,CAAC;IAE5E,CAAC;;;;;;;;AClXH,MAAa,QAAQ,OAAO,IAAI,kBAAkB;;;;AA8ClD,MAAa,iBAAiB,oBAA+B;;;;AAgG7D,SAAS,qBAAyC;AACjD,QAAO,IAAI,OAAO,YAAoB,YAAY,QAAQ,EAAE,EAC3D,IAAI,SAAS,MAAM;AAClB,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,SAAO,iBAAiB,MAAM,EAAE,CAAC;IAElC,CAAC;CAEF,SAAS,YAAY,SAAiB,OAA4B,EAAE,EAA0B;AAC7F,SAAO;GAAE;GAAS;GAAM;;CAGzB,SAAS,iBACR,KACA,MAC8C;EAC9C,MAAM,UAAU,CAAC,GAAG,MAAM,IAAI;EAE9B,MAAM,aAAa,YAAoB,YAAY,SAAS,QAAQ;AAEpE,SAAO,IAAI,MAAM,WAAW,EAC3B,IAAI,SAAS,MAAM;AAClB,OAAI,OAAO,SAAS,SAAU,QAAO;AAErC,OAAI,QAAQ,KAAK,KAAK,CACrB,QAAO,iBAAiB,SAAS,MAAM,GAAG,EAAE,QAAQ;AAGrD,UAAO,iBAAiB,MAAM,QAAQ;KAEvC,CAAC;;;;;;;AAYJ,SAAgB,eAA0B;CAEzC,MAAM,QADU,YAAY,CACN,MAAM,OAAO,eAAe;AAElD,KAAI,CAAC,MACJ,OAAM,IAAI,MAAM,uEAAuE;AAGxF,QAAO;;;;;;AAOR,SAAS,cAAc,QAA0C;CAEhE,MAAM,SADQ,cAAc,CACP,QAAQ,IAAIC,OAAK;AAEtC,KAAI,CAAC,OACJ,OAAM,IAAI,MAAM,uEAAuE;AAGxF,QAAO;;;;;AAMR,SAAgB,aACf,QACuC;AAEvC,QADc,cAAc,CACf,MAAM,IAAIA,OAAK;;;;;AAM7B,SAAgB,aACf,QACA,OACO;AAEP,CADc,cAAc,CACtB,MAAM,IAAIA,QAAM,MAAM;;AA+B7B,SAAgB,KACf,cACA,SACiB;CACjB,MAAM,KAAM,WAAW;CAEvB,MAAMC,SACL,CAAC,WAAW,iBAAiB,cAAc,OAAQ;CAEpD,MAAM,WAAW,EAAE;CAEnB,MAAMC,OAAiB;EACtB;EACA;EACA;AAGD,QAAO,eAAe,UAAU,UAAU;EACzC,OAAO;EACP,YAAY;EACZ,CAAC;AAGF,QAAO,eAAe,UAAU,UAAU;EACzC,MAAM;AAEL,UAAO,aADQ,cAAc,SAAS,CACX;;EAE5B,YAAY;EACZ,CAAC;AAGF,QAAO,eAAe,UAAU,UAAU,EACzC,MAAM;AACL,SAAO,aAAa,SAAS,EAAE;IAEhC,CAAC;AAGF,QAAO,eAAe,UAAU,UAAU,EACzC,MAAM;AACL,SAAO,iBACN,EAAE,QACK,aAAa,SAAS,EAAE,SAAqC,EAAE,GACrE,MAAM,UAAU;GAChB,MAAM,eAAe,aAAa,SAAS,IAAI,EAAE,OAAO,EAAE,EAAE;AAC5D,OAAI,KAAK,WAAW,EACnB,cAAa,UAAU;IAAE,GAAG;IAAc,OAAO;IAAO,CAAC;QACnD;IACN,MAAM,QAAS,aAAa,SAAqC,EAAE;AACnE,YAAQ,OAAO,KAAK,IAAI,OAAO,EAAE,MAAM;AACvC,iBAAa,UAAU;KAAE,GAAG;KAAc;KAAO,CAAC;;WAG9C,aAAa,SAAS,EAAE,UAAU,EAAE,CAC1C;IAEF,CAAC;AAGF,QAAO,eAAe,UAAU,eAAe,EAC9C,MAAM;AAEL,SAAO;GACN,MAAM;GACN,YAAY,aAHE,cAAc,SAAS,CAGL;GAChC;IAEF,CAAC;AAGF,QAAO,eAAe,UAAU,MAAM,EACrC,OAAO,MACP,CAAC;AAGF,QAAO,eAAe,UAAU,OAAO;EACtC,OAAO;EACP,YAAY;EACZ,CAAC;AAEF,QAAO;;;;;;AAWR,SAAS,sBAAsB,KAAuD;CACrF,MAAMC,SAAkC,EAAE;AAE1C,MAAK,MAAM,OAAO,OAAO,KAAK,IAAI,EAAE;AACnC,MAAI,IAAI,WAAW,IAAI,CAAE;EAEzB,MAAM,QAAQ,IAAI;AAElB,MAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,IAAI,EAAE,iBAAiB,MAC9F,QAAO,OAAO,sBAAsB,MAAiC;WAC3D,MAAM,QAAQ,MAAM,CAC9B,QAAO,OAAO,MAAM,KAAK,SACxB,SAAS,QAAQ,OAAO,SAAS,YAAY,EAAE,gBAAgB,QAC5D,sBAAsB,KAAgC,GACtD,KACH;MAED,QAAO,OAAO;;AAIhB,QAAO;;;;;;AAOR,eAAsB,YAAY,cAAsC,MAAqC;CAC5G,MAAM,EAAE,QAAQ,OAAO,aAAa;CAEpC,IAAI,gBAAgB;AAGpB,KAAI,QAAQ;EACX,MAAM,SAAS,MAAM,OAAO,aAAa,SAAS,KAAK;AAEvD,MAAI,OAAO,OACV,QAAO;GACN,QAAQ;GACR,QAAQ,cAAc,OAAO,OAAO,KAAK,YAAU,eAAeC,SAAO,KAAK,CAAC,CAAC;GAChF,OAAO,sBAAsB,KAAK;GAClC;AAEF,kBAAgB,OAAO;;CAIxB,MAAM,QAAQ,oBAAoB;AAElC,KAAI;AACH,SAAO;GACN,QAAQ,MAAM,GAAG,eAAe,MAAM;GACtC,QAAQ;GACR,OAAO;GACP;UACO,GAAG;AACX,MAAI,aAAa,gBAChB,QAAO;GACN,QAAQ;GACR,QAAQ,cAAc,EAAE,OAAO,KAAK,YAAU,eAAeA,SAAO,KAAK,CAAC,CAAC;GAC3E,OAAO,sBAAsB,KAAK;GAClC;AAGF,QAAM;;;;;;;;;AC7bR,SAAS,OAAO,OAAyC;AACxD,QAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkChE,SAAgB,MAAM,aAAgD;CACrE,MAAM,6BAAa,IAAI,SAA6C;CACpE,MAAM,4BAAY,IAAI,KAAqC;AAE3D,MAAK,MAAM,CAAC,MAAM,iBAAiB,OAAO,QAAQ,YAAY,EAAE;AAC/D,MAAI,CAAC,OAAO,aAAa,CACxB;EAGD,MAAM,IAAI;AAEV,aAAW,IAAI,GAAG,EAAE,IAAI,MAAM,CAAC;AAC/B,YAAU,IAAI,MAAM,EAAE;;AAGvB,QAAO,OAAO,SAAS,SAAS;EAC/B,MAAM,EAAE,KAAK,SAAS,UAAU;EAGhC,MAAMC,YAAuB;GAC5B,SAAS;GACT,uBAAO,IAAI,SAAS;GACpB;AAGD,QAAM,QAAQ,gBAAgB,UAAU;EAGxC,MAAM,SAAS,IAAI,aAAa,IAAI,WAAW;AAE/C,MAAI,UAAU,QAAQ,WAAW,QAAQ;GAExC,MAAM,eAAe,UAAU,IAAI,OAAO;AAE1C,OAAI,aASH,cAAa,cAHC,MAAM,YAAY,cAHnB,gBADI,MAAM,QAAQ,UAAU,CACoB,CAGH,CAGzB;;AAInC,SAAO,KAAK,QAAQ"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["result: Record<string, unknown>","values: unknown[]","current: Record<string, unknown>","current: unknown","normalized: InternalFormIssue","result: Record<string, InternalFormIssue[]>","target","baseProps: Record<string, unknown>","form","schema: StandardSchemaV1 | null","info: FormInfo","result: Record<string, unknown>","issue","formStore: FormStore"],"sources":["../src/lib/errors.ts","../src/lib/form-utils.ts","../src/lib/form.ts","../src/lib/middleware.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec';\n\n/**\n * error thrown when form validation fails imperatively\n */\nexport class ValidationError extends Error {\n\tissues: StandardSchemaV1.Issue[];\n\n\tconstructor(issues: StandardSchemaV1.Issue[]) {\n\t\tsuper('Validation failed');\n\t\tthis.name = 'ValidationError';\n\t\tthis.issues = issues;\n\t}\n}\n\n/**\n * use this to throw a validation error to imperatively fail form validation.\n * can be used in combination with `issue` passed to form actions to create field-specific issues.\n *\n * @example\n * ```ts\n * import { invalid, form } from '@oomfware/forms';\n * import * as v from 'valibot';\n *\n * export const login = form(\n * v.object({ name: v.string(), _password: v.string() }),\n * async ({ name, _password }, issue) => {\n * const success = tryLogin(name, _password);\n * if (!success) {\n * invalid('Incorrect username or password');\n * }\n *\n * // ...\n * }\n * );\n * ```\n */\nexport function invalid(...issues: (StandardSchemaV1.Issue | string)[]): never {\n\tthrow new ValidationError(issues.map((issue) => (typeof issue === 'string' ? { message: issue } : issue)));\n}\n\n/**\n * checks whether this is a validation error thrown by {@link invalid}.\n */\nexport function isValidationError(e: unknown): e is ValidationError {\n\treturn e instanceof ValidationError;\n}\n","import type { StandardSchemaV1 } from '@standard-schema/spec';\n\nimport type { FormIssue, InputType, InternalFormIssue } from './types.ts';\n\n/**\n * sets a value in a nested object using a path string, mutating the original object\n */\nexport function setNestedValue(object: Record<string, unknown>, pathString: string, value: unknown): void {\n\tif (pathString.startsWith('n:')) {\n\t\tpathString = pathString.slice(2);\n\t\tvalue = value === '' ? undefined : parseFloat(value as string);\n\t} else if (pathString.startsWith('b:')) {\n\t\tpathString = pathString.slice(2);\n\t\tvalue = value === 'on';\n\t}\n\n\tdeepSet(object, splitPath(pathString), value);\n}\n\n/**\n * convert `FormData` into a POJO\n */\nexport function convertFormData(data: FormData): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {};\n\n\tfor (let key of data.keys()) {\n\t\tconst isArray = key.endsWith('[]');\n\t\tlet values: unknown[] = data.getAll(key);\n\n\t\tif (isArray) {\n\t\t\tkey = key.slice(0, -2);\n\t\t}\n\n\t\tif (values.length > 1 && !isArray) {\n\t\t\tthrow new Error(`Form cannot contain duplicated keys — \"${key}\" has ${values.length} values`);\n\t\t}\n\n\t\t// an empty `<input type=\"file\">` will submit a non-existent file, bizarrely\n\t\tvalues = values.filter(\n\t\t\t(entry) => typeof entry === 'string' || (entry as File).name !== '' || (entry as File).size > 0,\n\t\t);\n\n\t\tif (key.startsWith('n:')) {\n\t\t\tkey = key.slice(2);\n\t\t\tvalues = values.map((v) => (v === '' ? undefined : parseFloat(v as string)));\n\t\t} else if (key.startsWith('b:')) {\n\t\t\tkey = key.slice(2);\n\t\t\tvalues = values.map((v) => v === 'on');\n\t\t}\n\n\t\tsetNestedValue(result, key, isArray ? values : values[0]);\n\t}\n\n\treturn result;\n}\n\nconst PATH_REGEX = /^[a-zA-Z_$]\\w*(\\.[a-zA-Z_$]\\w*|\\[\\d+\\])*$/;\n\n/**\n * splits a path string like \"user.emails[0].address\" into [\"user\", \"emails\", \"0\", \"address\"]\n */\nexport function splitPath(path: string): string[] {\n\tif (!PATH_REGEX.test(path)) {\n\t\tthrow new Error(`Invalid path ${path}`);\n\t}\n\n\treturn path.split(/\\.|\\[|\\]/).filter(Boolean);\n}\n\n/**\n * check if a property key is dangerous and could lead to prototype pollution\n */\nfunction checkPrototypePollution(key: string): void {\n\tif (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n\t\tthrow new Error(`Invalid key \"${key}\": This key is not allowed to prevent prototype pollution.`);\n\t}\n}\n\n/**\n * sets a value in a nested object using an array of keys, mutating the original object.\n */\nexport function deepSet(object: Record<string, unknown>, keys: string[], value: unknown): void {\n\tlet current: Record<string, unknown> = object;\n\n\tfor (let i = 0; i < keys.length - 1; i += 1) {\n\t\tconst key = keys[i]!;\n\n\t\tcheckPrototypePollution(key);\n\n\t\tconst isArray = /^\\d+$/.test(keys[i + 1]!);\n\t\tconst exists = key in current;\n\t\tconst inner = current[key];\n\n\t\tif (exists && isArray !== Array.isArray(inner)) {\n\t\t\tthrow new Error(`Invalid array key ${keys[i + 1]}`);\n\t\t}\n\n\t\tif (!exists) {\n\t\t\tcurrent[key] = isArray ? [] : {};\n\t\t}\n\n\t\tcurrent = current[key] as Record<string, unknown>;\n\t}\n\n\tconst finalKey = keys[keys.length - 1]!;\n\tcheckPrototypePollution(finalKey);\n\tcurrent[finalKey] = value;\n}\n\n/**\n * gets a nested value from an object using a path array\n */\nexport function deepGet(object: Record<string, unknown>, path: (string | number)[]): unknown {\n\tlet current: unknown = object;\n\tfor (const key of path) {\n\t\tif (current == null || typeof current !== 'object') {\n\t\t\treturn current;\n\t\t}\n\t\tcurrent = (current as Record<string | number, unknown>)[key];\n\t}\n\treturn current;\n}\n\n/**\n * normalizes a Standard Schema issue into our internal format\n */\nexport function normalizeIssue(issue: StandardSchemaV1.Issue, server = false): InternalFormIssue {\n\tconst normalized: InternalFormIssue = { name: '', path: [], message: issue.message, server };\n\n\tif (issue.path !== undefined) {\n\t\tlet name = '';\n\n\t\tfor (const segment of issue.path) {\n\t\t\tconst key = typeof segment === 'object' ? (segment.key as string | number) : segment;\n\n\t\t\tnormalized.path.push(key as string | number);\n\n\t\t\tif (typeof key === 'number') {\n\t\t\t\tname += `[${key}]`;\n\t\t\t} else if (typeof key === 'string') {\n\t\t\t\tname += name === '' ? key : '.' + key;\n\t\t\t}\n\t\t}\n\n\t\tnormalized.name = name;\n\t}\n\n\treturn normalized;\n}\n\n/**\n * flattens issues into a lookup object keyed by path\n * includes a special '$' key containing all issues\n */\nexport function flattenIssues(issues: InternalFormIssue[]): Record<string, InternalFormIssue[]> {\n\tconst result: Record<string, InternalFormIssue[]> = {};\n\n\tfor (const issue of issues) {\n\t\t(result.$ ??= []).push(issue);\n\n\t\tlet name = '';\n\n\t\tif (issue.path !== undefined) {\n\t\t\tfor (const key of issue.path) {\n\t\t\t\tif (typeof key === 'number') {\n\t\t\t\t\tname += `[${key}]`;\n\t\t\t\t} else if (typeof key === 'string') {\n\t\t\t\t\tname += name === '' ? key : '.' + key;\n\t\t\t\t}\n\n\t\t\t\t(result[name] ??= []).push(issue);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * builds a path string from an array of path segments\n */\nexport function buildPathString(path: (string | number)[]): string {\n\tlet result = '';\n\n\tfor (const segment of path) {\n\t\tif (typeof segment === 'number') {\n\t\t\tresult += `[${segment}]`;\n\t\t} else {\n\t\t\tresult += result === '' ? segment : '.' + segment;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n// #region field proxy\n\n/**\n * creates a proxy-based field accessor for form data.\n * allows type-safe nested field access like `fields.user.emails[0].address.value()`.\n */\nexport function createFieldProxy<T>(\n\ttarget: unknown,\n\tgetInput: () => Record<string, unknown>,\n\tsetInput: (path: (string | number)[], value: unknown) => void,\n\tgetIssues: () => Record<string, InternalFormIssue[]>,\n\tpath: (string | number)[] = [],\n): T {\n\tconst getValue = () => {\n\t\treturn deepGet(getInput(), path);\n\t};\n\n\treturn new Proxy(target as object, {\n\t\tget(target, prop) {\n\t\t\tif (typeof prop === 'symbol') return (target as Record<symbol, unknown>)[prop];\n\n\t\t\t// handle array access like jobs[0]\n\t\t\tif (/^\\d+$/.test(prop)) {\n\t\t\t\treturn createFieldProxy({}, getInput, setInput, getIssues, [...path, parseInt(prop, 10)]);\n\t\t\t}\n\n\t\t\tconst key = buildPathString(path);\n\n\t\t\tif (prop === 'set') {\n\t\t\t\tconst setFunc = function (newValue: unknown) {\n\t\t\t\t\tsetInput(path, newValue);\n\t\t\t\t\treturn newValue;\n\t\t\t\t};\n\t\t\t\treturn createFieldProxy(setFunc, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'value') {\n\t\t\t\treturn createFieldProxy(getValue, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'issues' || prop === 'allIssues') {\n\t\t\t\tconst issuesFunc = (): FormIssue[] | undefined => {\n\t\t\t\t\tconst allIssues = getIssues()[key === '' ? '$' : key];\n\n\t\t\t\t\tif (prop === 'allIssues') {\n\t\t\t\t\t\treturn allIssues?.map((issue) => ({\n\t\t\t\t\t\t\tpath: issue.path,\n\t\t\t\t\t\t\tmessage: issue.message,\n\t\t\t\t\t\t}));\n\t\t\t\t\t}\n\n\t\t\t\t\treturn allIssues\n\t\t\t\t\t\t?.filter((issue) => issue.name === key)\n\t\t\t\t\t\t?.map((issue) => ({\n\t\t\t\t\t\t\tpath: issue.path,\n\t\t\t\t\t\t\tmessage: issue.message,\n\t\t\t\t\t\t}));\n\t\t\t\t};\n\n\t\t\t\treturn createFieldProxy(issuesFunc, getInput, setInput, getIssues, [...path, prop]);\n\t\t\t}\n\n\t\t\tif (prop === 'as') {\n\t\t\t\tconst asFunc = (type: InputType, inputValue?: string): Record<string, unknown> => {\n\t\t\t\t\tconst isArray =\n\t\t\t\t\t\ttype === 'file multiple' ||\n\t\t\t\t\t\ttype === 'select multiple' ||\n\t\t\t\t\t\t(type === 'checkbox' && typeof inputValue === 'string');\n\n\t\t\t\t\tconst prefix =\n\t\t\t\t\t\ttype === 'number' || type === 'range' ? 'n:' : type === 'checkbox' && !isArray ? 'b:' : '';\n\n\t\t\t\t\t// base properties for all input types\n\t\t\t\t\tconst baseProps: Record<string, unknown> = {\n\t\t\t\t\t\tname: prefix + key + (isArray ? '[]' : ''),\n\t\t\t\t\t\tget 'aria-invalid'() {\n\t\t\t\t\t\t\tconst issues = getIssues();\n\t\t\t\t\t\t\treturn key in issues ? 'true' : undefined;\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\n\t\t\t\t\t// add type attribute only for non-text inputs and non-select elements\n\t\t\t\t\tif (type !== 'text' && type !== 'select' && type !== 'select multiple') {\n\t\t\t\t\t\tbaseProps.type = type === 'file multiple' ? 'file' : type;\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle submit and hidden inputs\n\t\t\t\t\tif (type === 'submit' || type === 'hidden') {\n\t\t\t\t\t\tif (!inputValue) {\n\t\t\t\t\t\t\tthrow new Error(`\\`${type}\\` inputs must have a value`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tvalue: { value: inputValue, enumerable: true },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle select inputs\n\t\t\t\t\tif (type === 'select' || type === 'select multiple') {\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tmultiple: { value: isArray, enumerable: true },\n\t\t\t\t\t\t\tvalue: {\n\t\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\t\treturn getValue();\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle checkbox inputs\n\t\t\t\t\tif (type === 'checkbox' || type === 'radio') {\n\t\t\t\t\t\tif (type === 'radio' && !inputValue) {\n\t\t\t\t\t\t\tthrow new Error('Radio inputs must have a value');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (type === 'checkbox' && isArray && !inputValue) {\n\t\t\t\t\t\t\tthrow new Error('Checkbox array inputs must have a value');\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tvalue: { value: inputValue ?? 'on', enumerable: true },\n\t\t\t\t\t\t\tchecked: {\n\t\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\t\tconst value = getValue();\n\n\t\t\t\t\t\t\t\t\tif (type === 'radio') {\n\t\t\t\t\t\t\t\t\t\treturn value === inputValue;\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tif (isArray) {\n\t\t\t\t\t\t\t\t\t\treturn ((value as string[] | undefined) ?? []).includes(inputValue!);\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle file inputs (can't persist value, just return name/type/multiple)\n\t\t\t\t\tif (type === 'file' || type === 'file multiple') {\n\t\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\t\tmultiple: { value: isArray, enumerable: true },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// handle all other input types (text, number, etc.)\n\t\t\t\t\treturn Object.defineProperties(baseProps, {\n\t\t\t\t\t\tvalue: {\n\t\t\t\t\t\t\tenumerable: true,\n\t\t\t\t\t\t\tget() {\n\t\t\t\t\t\t\t\tconst value = getValue();\n\t\t\t\t\t\t\t\treturn value != null ? String(value) : '';\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t};\n\n\t\t\t\treturn createFieldProxy(asFunc, getInput, setInput, getIssues, [...path, 'as']);\n\t\t\t}\n\n\t\t\t// handle property access (nested fields)\n\t\t\treturn createFieldProxy({}, getInput, setInput, getIssues, [...path, prop]);\n\t\t},\n\t}) as T;\n}\n\n// #endregion\n","import { createInjectionKey } from '@oomfware/fetch-router';\nimport { getContext } from '@oomfware/fetch-router/middlewares/async-context';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\n\nimport { ValidationError } from './errors.ts';\nimport { createFieldProxy, deepSet, flattenIssues, normalizeIssue } from './form-utils.ts';\nimport type { FormFields, FormInput, InternalFormIssue, MaybePromise } from './types.ts';\n\n// #region types\n\n/**\n * the issue creator proxy passed to form callbacks.\n * allows creating field-specific validation issues via property access.\n *\n * @example\n * ```ts\n * form(schema, async (data, issue) => {\n * if (emailTaken(data.email)) {\n * invalid(issue.email('Email already in use'));\n * }\n * // nested fields: issue.user.profile.name('Invalid name')\n * // array fields: issue.items[0].name('Invalid item name')\n * });\n * ```\n */\nexport type InvalidField<T> = ((message: string) => StandardSchemaV1.Issue) & {\n\t[K in keyof T]-?: T[K] extends (infer U)[]\n\t\t? InvalidFieldArray<U>\n\t\t: T[K] extends object\n\t\t\t? InvalidField<T[K]>\n\t\t\t: (message: string) => StandardSchemaV1.Issue;\n};\n\ntype InvalidFieldArray<T> = {\n\t[index: number]: T extends object ? InvalidField<T> : (message: string) => StandardSchemaV1.Issue;\n} & ((message: string) => StandardSchemaV1.Issue);\n\n/**\n * symbol used to identify form instances.\n */\nexport const kForm = Symbol.for('@oomfware/forms');\n\n/**\n * internal info attached to a form instance.\n * used by the forms() middleware to identify and process forms.\n */\nexport interface FormInfo {\n\t/** the schema, if any */\n\tschema: StandardSchemaV1 | null;\n\t/** the handler function */\n\tfn: (data: any, issue: any) => MaybePromise<any>;\n}\n\n/**\n * form config stored by the forms() middleware.\n */\nexport interface FormConfig {\n\t/** the form id, derived from registration name */\n\tid: string;\n}\n\n/**\n * form state stored by the forms() middleware.\n */\nexport interface FormState<Input = unknown, Output = unknown> {\n\t/** the submitted input data (for repopulating form on error) */\n\tinput?: Input;\n\t/** validation issues, flattened by path */\n\tissues?: Record<string, InternalFormIssue[]>;\n\t/** the handler result (if successful) */\n\tresult?: Output;\n}\n\n/**\n * the form store holds registered forms, their configs, and state.\n */\nexport interface FormStore {\n\t/** map of form instance to config */\n\tconfigs: WeakMap<InternalForm<any, any>, FormConfig>;\n\t/** state for each form instance */\n\tstate: WeakMap<InternalForm<any, any>, FormState>;\n}\n\n/**\n * injection key for the form store.\n */\nexport const FORM_STORE_KEY = createInjectionKey<FormStore>();\n\n/**\n * the return value of a form() function.\n * can be spread onto a <form> element.\n */\nexport interface Form<Input extends FormInput | void, Output> {\n\t/** HTTP method */\n\treadonly method: 'POST';\n\t/** the form action URL */\n\treadonly action: string;\n\t/** the handler result, if submission was successful */\n\treadonly result: Output | undefined;\n\t/** access form fields using object notation */\n\treadonly fields: FormFields<Input>;\n\t/** spread this onto a <button> or <input type=\"submit\"> */\n\treadonly buttonProps: FormButtonProps;\n}\n\n/**\n * internal form type with metadata.\n * used internally by middleware; cast Form to this when accessing `__`.\n */\nexport interface InternalForm<Input extends FormInput | void, Output> extends Form<Input, Output> {\n\t/** internal form info, used by forms() middleware */\n\treadonly __: FormInfo;\n}\n\nexport interface FormButtonProps {\n\ttype: 'submit';\n\treadonly formaction: string;\n}\n\n// #region issue creator\n\n/**\n * creates an issue creator proxy that builds up paths for field-specific issues.\n */\nfunction createIssueCreator<T>(): InvalidField<T> {\n\treturn new Proxy((message: string) => createIssue(message), {\n\t\tget(_target, prop) {\n\t\t\tif (typeof prop === 'symbol') return undefined;\n\t\t\treturn createIssueProxy(prop, []);\n\t\t},\n\t}) as InvalidField<T>;\n\n\tfunction createIssue(message: string, path: (string | number)[] = []): StandardSchemaV1.Issue {\n\t\treturn { message, path };\n\t}\n\n\tfunction createIssueProxy(\n\t\tkey: string | number,\n\t\tpath: (string | number)[],\n\t): (message: string) => StandardSchemaV1.Issue {\n\t\tconst newPath = [...path, key];\n\n\t\tconst issueFunc = (message: string) => createIssue(message, newPath);\n\n\t\treturn new Proxy(issueFunc, {\n\t\t\tget(_target, prop) {\n\t\t\t\tif (typeof prop === 'symbol') return undefined;\n\n\t\t\t\tif (/^\\d+$/.test(prop)) {\n\t\t\t\t\treturn createIssueProxy(parseInt(prop, 10), newPath);\n\t\t\t\t}\n\n\t\t\t\treturn createIssueProxy(prop, newPath);\n\t\t\t},\n\t\t});\n\t}\n}\n\n// #endregion\n\n// #region form state access\n\n/**\n * get the form store from the current request context.\n * @throws if called outside of a request context\n */\nexport function getFormStore(): FormStore {\n\tconst context = getContext();\n\tconst store = context.store.inject(FORM_STORE_KEY);\n\n\tif (!store) {\n\t\tthrow new Error('form store not found. make sure the forms() middleware is installed.');\n\t}\n\n\treturn store;\n}\n\n/**\n * get config for a specific form instance.\n * @throws if form is not registered with forms() middleware\n */\nfunction getFormConfig(form: InternalForm<any, any>): FormConfig {\n\tconst store = getFormStore();\n\tconst config = store.configs.get(form);\n\n\tif (!config) {\n\t\tthrow new Error('form not registered. make sure to pass it to the forms() middleware.');\n\t}\n\n\treturn config;\n}\n\n/**\n * get state for a specific form instance.\n */\nexport function getFormState<Input, Output>(\n\tform: InternalForm<any, any>,\n): FormState<Input, Output> | undefined {\n\tconst store = getFormStore();\n\treturn store.state.get(form) as FormState<Input, Output> | undefined;\n}\n\n/**\n * set state for a specific form instance.\n */\nexport function setFormState<Input, Output>(\n\tform: InternalForm<any, any>,\n\tstate: FormState<Input, Output>,\n): void {\n\tconst store = getFormStore();\n\tstore.state.set(form, state);\n}\n\n// #endregion\n\n// #region form function\n\n/**\n * creates a form without validation.\n */\nexport function form<Output>(fn: () => MaybePromise<Output>): Form<void, Output>;\n\n/**\n * creates a form with unchecked input (no validation).\n */\nexport function form<Input extends FormInput, Output>(\n\tvalidate: 'unchecked',\n\tfn: (data: Input, issue: InvalidField<Input>) => MaybePromise<Output>,\n): Form<Input, Output>;\n\n/**\n * creates a form with Standard Schema validation.\n */\nexport function form<Schema extends StandardSchemaV1<FormInput, Record<string, unknown>>, Output>(\n\tvalidate: Schema,\n\tfn: (\n\t\tdata: StandardSchemaV1.InferOutput<Schema>,\n\t\tissue: InvalidField<StandardSchemaV1.InferInput<Schema>>,\n\t) => MaybePromise<Output>,\n): Form<StandardSchemaV1.InferInput<Schema>, Output>;\n\nexport function form(\n\tvalidateOrFn: StandardSchemaV1 | 'unchecked' | (() => MaybePromise<unknown>),\n\tmaybeFn?: (data: any, issue: any) => MaybePromise<unknown>,\n): Form<any, any> {\n\tconst fn = (maybeFn ?? validateOrFn) as (data: any, issue: any) => MaybePromise<unknown>;\n\n\tconst schema: StandardSchemaV1 | null =\n\t\t!maybeFn || validateOrFn === 'unchecked' ? null : (validateOrFn as StandardSchemaV1);\n\n\tconst instance = {} as InternalForm<any, any>;\n\n\tconst info: FormInfo = {\n\t\tschema,\n\t\tfn,\n\t};\n\n\t// method\n\tObject.defineProperty(instance, 'method', {\n\t\tvalue: 'POST',\n\t\tenumerable: true,\n\t});\n\n\t// action - computed from form store\n\tObject.defineProperty(instance, 'action', {\n\t\tget() {\n\t\t\tconst config = getFormConfig(instance);\n\t\t\treturn `?__action=${config.id}`;\n\t\t},\n\t\tenumerable: true,\n\t});\n\n\t// result - from state store\n\tObject.defineProperty(instance, 'result', {\n\t\tget() {\n\t\t\treturn getFormState(instance)?.result;\n\t\t},\n\t});\n\n\t// fields - proxy for field access\n\tObject.defineProperty(instance, 'fields', {\n\t\tget() {\n\t\t\treturn createFieldProxy(\n\t\t\t\t{},\n\t\t\t\t() => (getFormState(instance)?.input as Record<string, unknown>) ?? {},\n\t\t\t\t(path, value) => {\n\t\t\t\t\tconst currentState = getFormState(instance) ?? { input: {} };\n\t\t\t\t\tif (path.length === 0) {\n\t\t\t\t\t\tsetFormState(instance, { ...currentState, input: value });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst input = (currentState.input as Record<string, unknown>) ?? {};\n\t\t\t\t\t\tdeepSet(input, path.map(String), value);\n\t\t\t\t\t\tsetFormState(instance, { ...currentState, input });\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t() => getFormState(instance)?.issues ?? {},\n\t\t\t);\n\t\t},\n\t});\n\n\t// buttonProps\n\tObject.defineProperty(instance, 'buttonProps', {\n\t\tget() {\n\t\t\tconst config = getFormConfig(instance);\n\t\t\treturn {\n\t\t\t\ttype: 'submit' as const,\n\t\t\t\tformaction: `?__action=${config.id}`,\n\t\t\t};\n\t\t},\n\t});\n\n\t// internal info\n\tObject.defineProperty(instance, '__', {\n\t\tvalue: info,\n\t});\n\n\t// brand symbol for identification\n\tObject.defineProperty(instance, kForm, {\n\t\tvalue: true,\n\t\tenumerable: false,\n\t});\n\n\treturn instance;\n}\n\n// #endregion\n\n// #region form processing\n\n/**\n * redacts sensitive fields (those starting with `_`) from form input.\n * this prevents passwords and other sensitive data from being returned in form state.\n */\nfunction redactSensitiveFields(obj: Record<string, unknown>): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {};\n\n\tfor (const key of Object.keys(obj)) {\n\t\tif (key.startsWith('_')) continue;\n\n\t\tconst value = obj[key];\n\n\t\tif (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof File)) {\n\t\t\tresult[key] = redactSensitiveFields(value as Record<string, unknown>);\n\t\t} else if (Array.isArray(value)) {\n\t\t\tresult[key] = value.map((item) =>\n\t\t\t\titem !== null && typeof item === 'object' && !(item instanceof File)\n\t\t\t\t\t? redactSensitiveFields(item as Record<string, unknown>)\n\t\t\t\t\t: item,\n\t\t\t);\n\t\t} else {\n\t\t\tresult[key] = value;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * process a form submission.\n * called by forms() middleware when a matching action is received.\n */\nexport async function processForm(formInstance: InternalForm<any, any>, data: FormInput): Promise<FormState> {\n\tconst { schema, fn } = formInstance.__;\n\n\tlet validatedData = data;\n\n\t// validate with schema if present\n\tif (schema) {\n\t\tconst result = await schema['~standard'].validate(data);\n\n\t\tif (result.issues) {\n\t\t\treturn {\n\t\t\t\tresult: undefined,\n\t\t\t\tissues: flattenIssues(result.issues.map((issue) => normalizeIssue(issue, true))),\n\t\t\t\tinput: redactSensitiveFields(data),\n\t\t\t};\n\t\t}\n\t\tvalidatedData = result.value as FormInput;\n\t}\n\n\t// run handler\n\tconst issue = createIssueCreator();\n\n\ttry {\n\t\treturn {\n\t\t\tresult: await fn(validatedData, issue),\n\t\t\tissues: undefined,\n\t\t\tinput: undefined,\n\t\t};\n\t} catch (e) {\n\t\tif (e instanceof ValidationError) {\n\t\t\treturn {\n\t\t\t\tresult: undefined,\n\t\t\t\tissues: flattenIssues(e.issues.map((issue) => normalizeIssue(issue, true))),\n\t\t\t\tinput: redactSensitiveFields(data),\n\t\t\t};\n\t\t}\n\n\t\tthrow e;\n\t}\n}\n\n// #endregion\n","import type { Middleware } from '@oomfware/fetch-router';\n\nimport { convertFormData } from './form-utils.ts';\nimport {\n\tFORM_STORE_KEY,\n\tkForm,\n\tprocessForm,\n\tsetFormState,\n\ttype Form,\n\ttype FormConfig,\n\ttype FormStore,\n\ttype InternalForm,\n} from './form.ts';\n\n// #region types\n\n/**\n * a record of form instances to register with the middleware.\n */\nexport type FormDefinitions = Record<string, Form<any, any>>;\n\n// #endregion\n\n// #region helpers\n\n/**\n * checks if a value is a form instance created by form().\n */\nfunction isForm(value: unknown): value is Form<any, any> {\n\treturn value !== null && typeof value === 'object' && kForm in value;\n}\n\n/**\n * checks if the request is a cross-origin request based on Sec-Fetch-Site header.\n * used for CSRF protection.\n */\nfunction isCrossOrigin(request: Request): boolean {\n\tconst secFetchSite = request.headers.get('sec-fetch-site');\n\treturn secFetchSite !== null && secFetchSite !== 'same-origin' && secFetchSite !== 'none';\n}\n\n// #endregion\n\n// #region middleware\n\n/**\n * creates a forms middleware that registers forms and handles form submissions.\n *\n * @example\n * ```ts\n * import { form, forms } from '@oomfware/forms';\n * import * as v from 'valibot';\n *\n * const createUserForm = form(\n * v.object({ name: v.string(), password: v.string() }),\n * async (input, issue) => {\n * // handle form submission\n * },\n * );\n *\n * router.map(routes.admin, {\n * middleware: [forms({ createUserForm })],\n * action() {\n * return render(\n * <form {...createUserForm}>\n * <input {...createUserForm.fields.name.as('text')} required />\n * </form>\n * );\n * },\n * });\n * ```\n */\nexport function forms(definitions: FormDefinitions): Middleware {\n\tconst formConfig = new WeakMap<InternalForm<any, any>, FormConfig>();\n\tconst formsById = new Map<string, InternalForm<any, any>>();\n\n\tfor (const [name, formInstance] of Object.entries(definitions)) {\n\t\tif (!isForm(formInstance)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst f = formInstance as InternalForm<any, any>;\n\n\t\tformConfig.set(f, { id: name });\n\t\tformsById.set(name, f);\n\t}\n\n\treturn async ({ request, url, store }, next) => {\n\t\t// create form store for this request\n\t\tconst formStore: FormStore = {\n\t\t\tconfigs: formConfig,\n\t\t\tstate: new WeakMap(),\n\t\t};\n\n\t\t// inject form store into context\n\t\tstore.provide(FORM_STORE_KEY, formStore);\n\n\t\t// check if this is a form submission\n\t\tconst action = url.searchParams.get('__action');\n\n\t\tif (action && request.method === 'POST') {\n\t\t\t// find the form\n\t\t\tconst formInstance = formsById.get(action);\n\n\t\t\tif (formInstance) {\n\t\t\t\t// reject cross-origin form submissions\n\t\t\t\tif (isCrossOrigin(request)) {\n\t\t\t\t\treturn new Response(null, { status: 403 });\n\t\t\t\t}\n\t\t\t\t// parse form data\n\t\t\t\tconst formData = await request.formData();\n\t\t\t\tconst data = convertFormData(formData as unknown as FormData);\n\n\t\t\t\t// process the form\n\t\t\t\tconst state = await processForm(formInstance, data as any);\n\n\t\t\t\t// store the state\n\t\t\t\tsetFormState(formInstance, state);\n\t\t\t}\n\t\t}\n\n\t\treturn next();\n\t};\n}\n\n// #endregion\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oomfware/forms",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": "form validation middleware",
|
|
6
6
|
"license": "0BSD",
|
|
7
7
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
|
29
|
-
"@oomfware/fetch-router": "^0.1
|
|
29
|
+
"@oomfware/fetch-router": "^0.2.1",
|
|
30
30
|
"@prettier/plugin-oxc": "^0.1.3",
|
|
31
31
|
"@types/bun": "^1.3.5",
|
|
32
32
|
"bumpp": "^10.3.2",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"valibot": "^1.2.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@oomfware/fetch-router": "^0.1
|
|
40
|
+
"@oomfware/fetch-router": "^0.2.1"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@standard-schema/spec": "^1.1.0"
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { form } from './lib/form.ts';
|
|
2
2
|
export { forms } from './lib/middleware.ts';
|
|
3
3
|
export { invalid, ValidationError, isValidationError } from './lib/errors.ts';
|
|
4
|
-
export type { Form
|
|
4
|
+
export type { Form } from './lib/form.ts';
|
|
5
|
+
export type { FormInput, FormIssue } from './lib/types.ts';
|
package/src/lib/form-utils.ts
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* internal representation of a form validation issue with computed path info
|
|
5
|
-
*/
|
|
6
|
-
export interface InternalFormIssue {
|
|
7
|
-
/** dot/bracket notation path string (e.g., "user.emails[0]") */
|
|
8
|
-
name: string;
|
|
9
|
-
/** path segments as array */
|
|
10
|
-
path: (string | number)[];
|
|
11
|
-
/** error message */
|
|
12
|
-
message: string;
|
|
13
|
-
/** whether this issue came from server validation */
|
|
14
|
-
server: boolean;
|
|
15
|
-
}
|
|
3
|
+
import type { FormIssue, InputType, InternalFormIssue } from './types.ts';
|
|
16
4
|
|
|
17
5
|
/**
|
|
18
6
|
* sets a value in a nested object using a path string, mutating the original object
|
|
@@ -207,60 +195,6 @@ export function buildPathString(path: (string | number)[]): string {
|
|
|
207
195
|
|
|
208
196
|
// #region field proxy
|
|
209
197
|
|
|
210
|
-
export interface FieldIssue {
|
|
211
|
-
path: (string | number)[];
|
|
212
|
-
message: string;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export interface FieldProxyMethods<T> {
|
|
216
|
-
/** get the current value of this field */
|
|
217
|
-
value(): T | undefined;
|
|
218
|
-
/** set the value of this field */
|
|
219
|
-
set(value: T): T;
|
|
220
|
-
/** get validation issues for this exact field */
|
|
221
|
-
issues(): FieldIssue[] | undefined;
|
|
222
|
-
/** get all validation issues for this field and its descendants */
|
|
223
|
-
allIssues(): FieldIssue[] | undefined;
|
|
224
|
-
/**
|
|
225
|
-
* get props for binding to an input element.
|
|
226
|
-
* returns an object with `name`, `aria-invalid`, and type-specific props.
|
|
227
|
-
*/
|
|
228
|
-
as(type: InputType, value?: string): InputProps;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export type InputType =
|
|
232
|
-
| 'text'
|
|
233
|
-
| 'number'
|
|
234
|
-
| 'range'
|
|
235
|
-
| 'checkbox'
|
|
236
|
-
| 'radio'
|
|
237
|
-
| 'file'
|
|
238
|
-
| 'file multiple'
|
|
239
|
-
| 'select'
|
|
240
|
-
| 'select multiple'
|
|
241
|
-
| 'hidden'
|
|
242
|
-
| 'submit'
|
|
243
|
-
| 'email'
|
|
244
|
-
| 'password'
|
|
245
|
-
| 'tel'
|
|
246
|
-
| 'url'
|
|
247
|
-
| 'date'
|
|
248
|
-
| 'time'
|
|
249
|
-
| 'datetime-local'
|
|
250
|
-
| 'month'
|
|
251
|
-
| 'week'
|
|
252
|
-
| 'color'
|
|
253
|
-
| 'search';
|
|
254
|
-
|
|
255
|
-
export interface InputProps {
|
|
256
|
-
name: string;
|
|
257
|
-
'aria-invalid'?: 'true';
|
|
258
|
-
type?: string;
|
|
259
|
-
value?: string;
|
|
260
|
-
checked?: boolean;
|
|
261
|
-
multiple?: boolean;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
198
|
/**
|
|
265
199
|
* creates a proxy-based field accessor for form data.
|
|
266
200
|
* allows type-safe nested field access like `fields.user.emails[0].address.value()`.
|
|
@@ -280,7 +214,7 @@ export function createFieldProxy<T>(
|
|
|
280
214
|
get(target, prop) {
|
|
281
215
|
if (typeof prop === 'symbol') return (target as Record<symbol, unknown>)[prop];
|
|
282
216
|
|
|
283
|
-
//
|
|
217
|
+
// handle array access like jobs[0]
|
|
284
218
|
if (/^\d+$/.test(prop)) {
|
|
285
219
|
return createFieldProxy({}, getInput, setInput, getIssues, [...path, parseInt(prop, 10)]);
|
|
286
220
|
}
|
|
@@ -300,7 +234,7 @@ export function createFieldProxy<T>(
|
|
|
300
234
|
}
|
|
301
235
|
|
|
302
236
|
if (prop === 'issues' || prop === 'allIssues') {
|
|
303
|
-
const issuesFunc = ():
|
|
237
|
+
const issuesFunc = (): FormIssue[] | undefined => {
|
|
304
238
|
const allIssues = getIssues()[key === '' ? '$' : key];
|
|
305
239
|
|
|
306
240
|
if (prop === 'allIssues') {
|
|
@@ -322,7 +256,7 @@ export function createFieldProxy<T>(
|
|
|
322
256
|
}
|
|
323
257
|
|
|
324
258
|
if (prop === 'as') {
|
|
325
|
-
const asFunc = (type: InputType, inputValue?: string):
|
|
259
|
+
const asFunc = (type: InputType, inputValue?: string): Record<string, unknown> => {
|
|
326
260
|
const isArray =
|
|
327
261
|
type === 'file multiple' ||
|
|
328
262
|
type === 'select multiple' ||
|
|
@@ -331,8 +265,8 @@ export function createFieldProxy<T>(
|
|
|
331
265
|
const prefix =
|
|
332
266
|
type === 'number' || type === 'range' ? 'n:' : type === 'checkbox' && !isArray ? 'b:' : '';
|
|
333
267
|
|
|
334
|
-
//
|
|
335
|
-
const baseProps:
|
|
268
|
+
// base properties for all input types
|
|
269
|
+
const baseProps: Record<string, unknown> = {
|
|
336
270
|
name: prefix + key + (isArray ? '[]' : ''),
|
|
337
271
|
get 'aria-invalid'() {
|
|
338
272
|
const issues = getIssues();
|
|
@@ -340,12 +274,12 @@ export function createFieldProxy<T>(
|
|
|
340
274
|
},
|
|
341
275
|
};
|
|
342
276
|
|
|
343
|
-
//
|
|
277
|
+
// add type attribute only for non-text inputs and non-select elements
|
|
344
278
|
if (type !== 'text' && type !== 'select' && type !== 'select multiple') {
|
|
345
279
|
baseProps.type = type === 'file multiple' ? 'file' : type;
|
|
346
280
|
}
|
|
347
281
|
|
|
348
|
-
//
|
|
282
|
+
// handle submit and hidden inputs
|
|
349
283
|
if (type === 'submit' || type === 'hidden') {
|
|
350
284
|
if (!inputValue) {
|
|
351
285
|
throw new Error(`\`${type}\` inputs must have a value`);
|
|
@@ -356,7 +290,7 @@ export function createFieldProxy<T>(
|
|
|
356
290
|
});
|
|
357
291
|
}
|
|
358
292
|
|
|
359
|
-
//
|
|
293
|
+
// handle select inputs
|
|
360
294
|
if (type === 'select' || type === 'select multiple') {
|
|
361
295
|
return Object.defineProperties(baseProps, {
|
|
362
296
|
multiple: { value: isArray, enumerable: true },
|
|
@@ -369,7 +303,7 @@ export function createFieldProxy<T>(
|
|
|
369
303
|
});
|
|
370
304
|
}
|
|
371
305
|
|
|
372
|
-
//
|
|
306
|
+
// handle checkbox inputs
|
|
373
307
|
if (type === 'checkbox' || type === 'radio') {
|
|
374
308
|
if (type === 'radio' && !inputValue) {
|
|
375
309
|
throw new Error('Radio inputs must have a value');
|
|
@@ -400,14 +334,14 @@ export function createFieldProxy<T>(
|
|
|
400
334
|
});
|
|
401
335
|
}
|
|
402
336
|
|
|
403
|
-
//
|
|
337
|
+
// handle file inputs (can't persist value, just return name/type/multiple)
|
|
404
338
|
if (type === 'file' || type === 'file multiple') {
|
|
405
339
|
return Object.defineProperties(baseProps, {
|
|
406
340
|
multiple: { value: isArray, enumerable: true },
|
|
407
341
|
});
|
|
408
342
|
}
|
|
409
343
|
|
|
410
|
-
//
|
|
344
|
+
// handle all other input types (text, number, etc.)
|
|
411
345
|
return Object.defineProperties(baseProps, {
|
|
412
346
|
value: {
|
|
413
347
|
enumerable: true,
|
|
@@ -422,7 +356,7 @@ export function createFieldProxy<T>(
|
|
|
422
356
|
return createFieldProxy(asFunc, getInput, setInput, getIssues, [...path, 'as']);
|
|
423
357
|
}
|
|
424
358
|
|
|
425
|
-
//
|
|
359
|
+
// handle property access (nested fields)
|
|
426
360
|
return createFieldProxy({}, getInput, setInput, getIssues, [...path, prop]);
|
|
427
361
|
},
|
|
428
362
|
}) as T;
|
package/src/lib/form.ts
CHANGED
|
@@ -3,28 +3,11 @@ import { getContext } from '@oomfware/fetch-router/middlewares/async-context';
|
|
|
3
3
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
4
4
|
|
|
5
5
|
import { ValidationError } from './errors.ts';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
deepSet,
|
|
9
|
-
flattenIssues,
|
|
10
|
-
normalizeIssue,
|
|
11
|
-
type InternalFormIssue,
|
|
12
|
-
} from './form-utils.ts';
|
|
13
|
-
import type { MaybePromise } from './types.ts';
|
|
6
|
+
import { createFieldProxy, deepSet, flattenIssues, normalizeIssue } from './form-utils.ts';
|
|
7
|
+
import type { FormFields, FormInput, InternalFormIssue, MaybePromise } from './types.ts';
|
|
14
8
|
|
|
15
9
|
// #region types
|
|
16
10
|
|
|
17
|
-
export interface FormInput {
|
|
18
|
-
[key: string]: MaybeArray<string | number | boolean | File | FormInput>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type MaybeArray<T> = T | T[];
|
|
22
|
-
|
|
23
|
-
export interface FormIssue {
|
|
24
|
-
message: string;
|
|
25
|
-
path: (string | number)[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
11
|
/**
|
|
29
12
|
* the issue creator proxy passed to form callbacks.
|
|
30
13
|
* allows creating field-specific validation issues via property access.
|
|
@@ -134,64 +117,6 @@ export interface FormButtonProps {
|
|
|
134
117
|
readonly formaction: string;
|
|
135
118
|
}
|
|
136
119
|
|
|
137
|
-
// #region field types
|
|
138
|
-
|
|
139
|
-
/** valid leaf value types for form fields */
|
|
140
|
-
export type FormFieldValue = string | string[] | number | boolean | File | File[];
|
|
141
|
-
|
|
142
|
-
/** guard to prevent infinite recursion when T is unknown or has an index signature */
|
|
143
|
-
type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
|
|
144
|
-
|
|
145
|
-
/** base methods available on all form fields */
|
|
146
|
-
export interface FormFieldMethods<T> {
|
|
147
|
-
/** get the current value */
|
|
148
|
-
value(): T | undefined;
|
|
149
|
-
/** set the value */
|
|
150
|
-
set(value: T): T;
|
|
151
|
-
/** get validation issues for this field */
|
|
152
|
-
issues(): FormIssue[] | undefined;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** leaf field (primitives, files) with .as() method */
|
|
156
|
-
export type FormFieldLeaf<T extends FormFieldValue> = FormFieldMethods<T> & {
|
|
157
|
-
/** get props for an input element */
|
|
158
|
-
as(type: string, value?: string): Record<string, unknown>;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
/** container field (objects, arrays) with allIssues() method */
|
|
162
|
-
type FormFieldContainer<T> = FormFieldMethods<T> & {
|
|
163
|
-
/** get all issues for this field and descendants */
|
|
164
|
-
allIssues(): FormIssue[] | undefined;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
/** fallback field type when recursion would be infinite */
|
|
168
|
-
type FormFieldUnknown<T> = FormFieldMethods<T> & {
|
|
169
|
-
/** get all issues for this field and descendants */
|
|
170
|
-
allIssues(): FormIssue[] | undefined;
|
|
171
|
-
/** get props for an input element */
|
|
172
|
-
as(type: string, value?: string): Record<string, unknown>;
|
|
173
|
-
} & {
|
|
174
|
-
[key: string | number]: FormFieldUnknown<unknown>;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* recursive type to build form fields structure with proxy access.
|
|
179
|
-
* preserves type information through the object hierarchy.
|
|
180
|
-
*/
|
|
181
|
-
export type FormFields<T> = T extends void
|
|
182
|
-
? Record<string, never>
|
|
183
|
-
: WillRecurseIndefinitely<T> extends true
|
|
184
|
-
? FormFieldUnknown<T>
|
|
185
|
-
: NonNullable<T> extends string | number | boolean | File
|
|
186
|
-
? FormFieldLeaf<NonNullable<T>>
|
|
187
|
-
: T extends string[] | File[]
|
|
188
|
-
? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> }
|
|
189
|
-
: T extends Array<infer U>
|
|
190
|
-
? FormFieldContainer<T> & { [K in number]: FormFields<U> }
|
|
191
|
-
: FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };
|
|
192
|
-
|
|
193
|
-
// #endregion
|
|
194
|
-
|
|
195
120
|
// #region issue creator
|
|
196
121
|
|
|
197
122
|
/**
|
package/src/lib/middleware.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Middleware } from '@oomfware/fetch-router';
|
|
2
2
|
|
|
3
3
|
import { convertFormData } from './form-utils.ts';
|
|
4
4
|
import {
|
|
@@ -30,6 +30,15 @@ function isForm(value: unknown): value is Form<any, any> {
|
|
|
30
30
|
return value !== null && typeof value === 'object' && kForm in value;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* checks if the request is a cross-origin request based on Sec-Fetch-Site header.
|
|
35
|
+
* used for CSRF protection.
|
|
36
|
+
*/
|
|
37
|
+
function isCrossOrigin(request: Request): boolean {
|
|
38
|
+
const secFetchSite = request.headers.get('sec-fetch-site');
|
|
39
|
+
return secFetchSite !== null && secFetchSite !== 'same-origin' && secFetchSite !== 'none';
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
// #endregion
|
|
34
43
|
|
|
35
44
|
// #region middleware
|
|
@@ -61,7 +70,7 @@ function isForm(value: unknown): value is Form<any, any> {
|
|
|
61
70
|
* });
|
|
62
71
|
* ```
|
|
63
72
|
*/
|
|
64
|
-
export function forms(definitions: FormDefinitions):
|
|
73
|
+
export function forms(definitions: FormDefinitions): Middleware {
|
|
65
74
|
const formConfig = new WeakMap<InternalForm<any, any>, FormConfig>();
|
|
66
75
|
const formsById = new Map<string, InternalForm<any, any>>();
|
|
67
76
|
|
|
@@ -76,9 +85,7 @@ export function forms(definitions: FormDefinitions): RouterMiddleware {
|
|
|
76
85
|
formsById.set(name, f);
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
return async (
|
|
80
|
-
const { url, request, store } = context;
|
|
81
|
-
|
|
88
|
+
return async ({ request, url, store }, next) => {
|
|
82
89
|
// create form store for this request
|
|
83
90
|
const formStore: FormStore = {
|
|
84
91
|
configs: formConfig,
|
|
@@ -96,6 +103,10 @@ export function forms(definitions: FormDefinitions): RouterMiddleware {
|
|
|
96
103
|
const formInstance = formsById.get(action);
|
|
97
104
|
|
|
98
105
|
if (formInstance) {
|
|
106
|
+
// reject cross-origin form submissions
|
|
107
|
+
if (isCrossOrigin(request)) {
|
|
108
|
+
return new Response(null, { status: 403 });
|
|
109
|
+
}
|
|
99
110
|
// parse form data
|
|
100
111
|
const formData = await request.formData();
|
|
101
112
|
const data = convertFormData(formData as unknown as FormData);
|
|
@@ -108,7 +119,7 @@ export function forms(definitions: FormDefinitions): RouterMiddleware {
|
|
|
108
119
|
}
|
|
109
120
|
}
|
|
110
121
|
|
|
111
|
-
return next(
|
|
122
|
+
return next();
|
|
112
123
|
};
|
|
113
124
|
}
|
|
114
125
|
|
package/src/lib/types.ts
CHANGED
|
@@ -1 +1,242 @@
|
|
|
1
|
+
// #region utility types
|
|
2
|
+
|
|
1
3
|
export type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
|
|
5
|
+
type MaybeArray<T> = T | T[];
|
|
6
|
+
|
|
7
|
+
// #endregion
|
|
8
|
+
|
|
9
|
+
// #region form input types
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* valid structure for form input data
|
|
13
|
+
*/
|
|
14
|
+
export interface FormInput {
|
|
15
|
+
[key: string]: MaybeArray<string | number | boolean | File | FormInput>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* a validation issue with path information
|
|
20
|
+
*/
|
|
21
|
+
export interface FormIssue {
|
|
22
|
+
message: string;
|
|
23
|
+
path: (string | number)[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* internal representation of a form validation issue with computed path info
|
|
28
|
+
*/
|
|
29
|
+
export interface InternalFormIssue {
|
|
30
|
+
/** dot/bracket notation path string (e.g., "user.emails[0]") */
|
|
31
|
+
name: string;
|
|
32
|
+
/** path segments as array */
|
|
33
|
+
path: (string | number)[];
|
|
34
|
+
/** error message */
|
|
35
|
+
message: string;
|
|
36
|
+
/** whether this issue came from server validation */
|
|
37
|
+
server: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// #endregion
|
|
41
|
+
|
|
42
|
+
// #region input type mapping
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* maps input types to their corresponding value types
|
|
46
|
+
*/
|
|
47
|
+
export type InputTypeMap = {
|
|
48
|
+
text: string;
|
|
49
|
+
email: string;
|
|
50
|
+
password: string;
|
|
51
|
+
url: string;
|
|
52
|
+
tel: string;
|
|
53
|
+
search: string;
|
|
54
|
+
number: number;
|
|
55
|
+
range: number;
|
|
56
|
+
date: string;
|
|
57
|
+
'datetime-local': string;
|
|
58
|
+
time: string;
|
|
59
|
+
month: string;
|
|
60
|
+
week: string;
|
|
61
|
+
color: string;
|
|
62
|
+
checkbox: boolean | string[];
|
|
63
|
+
radio: string;
|
|
64
|
+
file: File;
|
|
65
|
+
'file multiple': File[];
|
|
66
|
+
hidden: string;
|
|
67
|
+
submit: string;
|
|
68
|
+
button: string;
|
|
69
|
+
reset: string;
|
|
70
|
+
image: string;
|
|
71
|
+
select: string;
|
|
72
|
+
'select multiple': string[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* all valid input type strings
|
|
77
|
+
*/
|
|
78
|
+
export type InputType = keyof InputTypeMap;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* valid input types for a given value type
|
|
82
|
+
*/
|
|
83
|
+
export type FieldInputType<T> = {
|
|
84
|
+
[K in keyof InputTypeMap]: T extends InputTypeMap[K] ? K : never;
|
|
85
|
+
}[keyof InputTypeMap];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* arguments for the `as()` method based on input type and field value
|
|
89
|
+
*/
|
|
90
|
+
export type AsArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox'
|
|
91
|
+
? Value extends string[]
|
|
92
|
+
? [type: Type, value: Value[number] | (string & {})]
|
|
93
|
+
: [type: Type]
|
|
94
|
+
: Type extends 'radio' | 'submit' | 'hidden'
|
|
95
|
+
? [type: Type, value: Value | (string & {})]
|
|
96
|
+
: [type: Type];
|
|
97
|
+
|
|
98
|
+
// #endregion
|
|
99
|
+
|
|
100
|
+
// #region input element props
|
|
101
|
+
|
|
102
|
+
interface CheckboxRadioProps<T extends 'checkbox' | 'radio'> {
|
|
103
|
+
name: string;
|
|
104
|
+
type: T;
|
|
105
|
+
value: string;
|
|
106
|
+
'aria-invalid'?: 'true';
|
|
107
|
+
readonly checked: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface FileProps {
|
|
111
|
+
name: string;
|
|
112
|
+
type: 'file';
|
|
113
|
+
'aria-invalid'?: 'true';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface FileMultipleProps {
|
|
117
|
+
name: string;
|
|
118
|
+
type: 'file';
|
|
119
|
+
multiple: true;
|
|
120
|
+
'aria-invalid'?: 'true';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface SelectProps {
|
|
124
|
+
name: string;
|
|
125
|
+
multiple: false;
|
|
126
|
+
'aria-invalid'?: 'true';
|
|
127
|
+
readonly value: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface SelectMultipleProps {
|
|
131
|
+
name: string;
|
|
132
|
+
multiple: true;
|
|
133
|
+
'aria-invalid'?: 'true';
|
|
134
|
+
readonly value: string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface TextProps {
|
|
138
|
+
name: string;
|
|
139
|
+
'aria-invalid'?: 'true';
|
|
140
|
+
readonly value: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface TypedInputProps<T extends string> {
|
|
144
|
+
name: string;
|
|
145
|
+
type: T;
|
|
146
|
+
'aria-invalid'?: 'true';
|
|
147
|
+
readonly value: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* input element properties based on input type
|
|
152
|
+
*/
|
|
153
|
+
export type InputElementProps<T extends keyof InputTypeMap> = T extends 'checkbox' | 'radio'
|
|
154
|
+
? CheckboxRadioProps<T>
|
|
155
|
+
: T extends 'file'
|
|
156
|
+
? FileProps
|
|
157
|
+
: T extends 'file multiple'
|
|
158
|
+
? FileMultipleProps
|
|
159
|
+
: T extends 'select'
|
|
160
|
+
? SelectProps
|
|
161
|
+
: T extends 'select multiple'
|
|
162
|
+
? SelectMultipleProps
|
|
163
|
+
: T extends 'text'
|
|
164
|
+
? TextProps
|
|
165
|
+
: TypedInputProps<T>;
|
|
166
|
+
|
|
167
|
+
// #endregion
|
|
168
|
+
|
|
169
|
+
// #region field types
|
|
170
|
+
|
|
171
|
+
/** valid leaf value types for form fields */
|
|
172
|
+
export type FormFieldValue = string | string[] | number | boolean | File | File[];
|
|
173
|
+
|
|
174
|
+
/** guard to prevent infinite recursion when T is unknown or has an index signature */
|
|
175
|
+
type WillRecurseIndefinitely<T> = unknown extends T ? true : string extends keyof T ? true : false;
|
|
176
|
+
|
|
177
|
+
/** base methods available on all form fields */
|
|
178
|
+
interface FieldMethods<T> {
|
|
179
|
+
/** get the current value */
|
|
180
|
+
value(): T | undefined;
|
|
181
|
+
/** set the value */
|
|
182
|
+
set(value: T): T;
|
|
183
|
+
/** get validation issues for this field */
|
|
184
|
+
issues(): FormIssue[] | undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* leaf field type for primitive values with `.as()` method
|
|
189
|
+
*/
|
|
190
|
+
export type FormFieldLeaf<T extends FormFieldValue> = FieldMethods<T> & {
|
|
191
|
+
/**
|
|
192
|
+
* get props for binding to an input element.
|
|
193
|
+
* returns an object with `name`, `aria-invalid`, and type-specific props.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* <input {...fields.name.as('text')} />
|
|
198
|
+
* <input {...fields.age.as('number')} />
|
|
199
|
+
* <input {...fields.agreed.as('checkbox')} />
|
|
200
|
+
* <input {...fields.color.as('radio', 'red')} />
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
as<K extends FieldInputType<T>>(...args: AsArgs<K, T>): InputElementProps<K>;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* container field type for objects/arrays with `.allIssues()` method
|
|
208
|
+
*/
|
|
209
|
+
export type FormFieldContainer<T> = FieldMethods<T> & {
|
|
210
|
+
/** get all issues for this field and descendants */
|
|
211
|
+
allIssues(): FormIssue[] | undefined;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* fallback field type when recursion would be infinite
|
|
216
|
+
*/
|
|
217
|
+
type FormFieldUnknown<T> = FieldMethods<T> & {
|
|
218
|
+
/** get all issues for this field and descendants */
|
|
219
|
+
allIssues(): FormIssue[] | undefined;
|
|
220
|
+
/** get props for an input element */
|
|
221
|
+
as<K extends FieldInputType<FormFieldValue>>(...args: AsArgs<K, FormFieldValue>): InputElementProps<K>;
|
|
222
|
+
} & {
|
|
223
|
+
[key: string | number]: FormFieldUnknown<unknown>;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* recursive type to build form fields structure with proxy access.
|
|
228
|
+
* preserves type information through the object hierarchy.
|
|
229
|
+
*/
|
|
230
|
+
export type FormFields<T> = T extends void
|
|
231
|
+
? Record<string, never>
|
|
232
|
+
: WillRecurseIndefinitely<T> extends true
|
|
233
|
+
? FormFieldUnknown<T>
|
|
234
|
+
: NonNullable<T> extends string | number | boolean | File
|
|
235
|
+
? FormFieldLeaf<NonNullable<T>>
|
|
236
|
+
: T extends string[] | File[]
|
|
237
|
+
? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> }
|
|
238
|
+
: T extends Array<infer U>
|
|
239
|
+
? FormFieldContainer<T> & { [K in number]: FormFields<U> }
|
|
240
|
+
: FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };
|
|
241
|
+
|
|
242
|
+
// #endregion
|