@oomfware/forms 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +40 -14
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +34 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/lib/form.ts +78 -20
- package/src/lib/types.ts +28 -16
package/dist/index.d.mts
CHANGED
|
@@ -52,9 +52,14 @@ type InputTypeMap = {
|
|
|
52
52
|
*/
|
|
53
53
|
type FieldInputType<T> = { [K in keyof InputTypeMap]: T extends InputTypeMap[K] ? K : never }[keyof InputTypeMap];
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
55
|
+
* value argument for the `as()` method based on input type.
|
|
56
|
+
* returns `[value]` tuple for types that require a value, or `[]` otherwise.
|
|
57
|
+
*
|
|
58
|
+
* note: separating `type` from `value` in the `.as()` signature ensures TypeScript
|
|
59
|
+
* can properly infer the type parameter before resolving the value constraint.
|
|
60
|
+
* using `...args: [type, value?]` with a union of tuple types causes inference issues.
|
|
56
61
|
*/
|
|
57
|
-
type
|
|
62
|
+
type AsValueArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox' ? Value extends string[] ? [value: Value[number] | (string & {})] : [] : Type extends 'radio' | 'submit' | 'hidden' ? [value: Value | (string & {})] : [];
|
|
58
63
|
interface CheckboxRadioProps<T extends 'checkbox' | 'radio'> {
|
|
59
64
|
name: string;
|
|
60
65
|
type: T;
|
|
@@ -129,7 +134,7 @@ type FormFieldLeaf<T extends FormFieldValue> = FieldMethods<T> & {
|
|
|
129
134
|
* <input {...fields.color.as('radio', 'red')} />
|
|
130
135
|
* ```
|
|
131
136
|
*/
|
|
132
|
-
as<K$1 extends FieldInputType<T>>(...
|
|
137
|
+
as<K$1 extends FieldInputType<T>>(type: K$1, ...value: AsValueArgs<K$1, T>): InputElementProps<K$1>;
|
|
133
138
|
};
|
|
134
139
|
/**
|
|
135
140
|
* container field type for objects/arrays with `.allIssues()` method
|
|
@@ -141,19 +146,22 @@ type FormFieldContainer<T> = FieldMethods<T> & {
|
|
|
141
146
|
/**
|
|
142
147
|
* fallback field type when recursion would be infinite
|
|
143
148
|
*/
|
|
144
|
-
type
|
|
149
|
+
type UnknownField<T> = FieldMethods<T> & {
|
|
145
150
|
/** get all issues for this field and descendants */
|
|
146
151
|
allIssues(): FormIssue[] | undefined;
|
|
147
152
|
/** get props for an input element */
|
|
148
|
-
as<K$1 extends FieldInputType<FormFieldValue>>(...
|
|
153
|
+
as<K$1 extends FieldInputType<FormFieldValue>>(type: K$1, ...value: AsValueArgs<K$1, FormFieldValue>): InputElementProps<K$1>;
|
|
149
154
|
} & {
|
|
150
|
-
[key: string | number]:
|
|
155
|
+
[key: string | number]: UnknownField<any>;
|
|
156
|
+
};
|
|
157
|
+
type RecursiveFormFields = FormFieldContainer<any> & {
|
|
158
|
+
[key: string | number]: UnknownField<any>;
|
|
151
159
|
};
|
|
152
160
|
/**
|
|
153
161
|
* recursive type to build form fields structure with proxy access.
|
|
154
162
|
* preserves type information through the object hierarchy.
|
|
155
163
|
*/
|
|
156
|
-
type FormFields<T> =
|
|
164
|
+
type FormFields<T> = WillRecurseIndefinitely<T> extends true ? RecursiveFormFields : 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
165
|
//#endregion
|
|
158
166
|
//#region src/lib/form.d.ts
|
|
159
167
|
/**
|
|
@@ -179,6 +187,16 @@ type InvalidFieldArray<T> = {
|
|
|
179
187
|
* symbol used to identify form instances.
|
|
180
188
|
*/
|
|
181
189
|
|
|
190
|
+
/**
|
|
191
|
+
* options for configuring form behavior.
|
|
192
|
+
*/
|
|
193
|
+
interface FormOptions {
|
|
194
|
+
/**
|
|
195
|
+
* if true, preserves existing search params instead of replacing them.
|
|
196
|
+
* @default false
|
|
197
|
+
*/
|
|
198
|
+
preserveParams?: boolean;
|
|
199
|
+
}
|
|
182
200
|
/**
|
|
183
201
|
* the return value of a form() function.
|
|
184
202
|
* can be spread onto a <form> element.
|
|
@@ -192,12 +210,12 @@ interface Form<Input extends FormInput | void, Output> {
|
|
|
192
210
|
readonly result: Output | undefined;
|
|
193
211
|
/** access form fields using object notation */
|
|
194
212
|
readonly fields: FormFields<Input>;
|
|
195
|
-
/**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
213
|
+
/**
|
|
214
|
+
* returns a configured form with the given options.
|
|
215
|
+
* @param options form configuration options
|
|
216
|
+
* @returns a new form object with the options applied
|
|
217
|
+
*/
|
|
218
|
+
with(options: FormOptions): ConfiguredForm;
|
|
201
219
|
}
|
|
202
220
|
/**
|
|
203
221
|
* creates a form without validation.
|
|
@@ -211,6 +229,14 @@ declare function form<Input extends FormInput, Output>(validate: 'unchecked', fn
|
|
|
211
229
|
* creates a form with Standard Schema validation.
|
|
212
230
|
*/
|
|
213
231
|
declare function form<Schema extends StandardSchemaV1<FormInput, Record<string, unknown>>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput<Schema>, issue: InvalidField<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>): Form<StandardSchemaV1.InferInput<Schema>, Output>;
|
|
232
|
+
/**
|
|
233
|
+
* configured form props for spreading onto a form element.
|
|
234
|
+
*/
|
|
235
|
+
interface ConfiguredForm {
|
|
236
|
+
readonly method: 'POST';
|
|
237
|
+
readonly action: string;
|
|
238
|
+
with(options: FormOptions): ConfiguredForm;
|
|
239
|
+
}
|
|
214
240
|
//#endregion
|
|
215
241
|
//#region src/lib/middleware.d.ts
|
|
216
242
|
/**
|
|
@@ -282,5 +308,5 @@ declare function invalid(...issues: (StandardSchemaV1.Issue | string)[]): never;
|
|
|
282
308
|
*/
|
|
283
309
|
declare function isValidationError(e: unknown): e is ValidationError;
|
|
284
310
|
//#endregion
|
|
285
|
-
export { type Form, type FormInput, type FormIssue, ValidationError, form, forms, invalid, isValidationError };
|
|
311
|
+
export { type ConfiguredForm, type Form, type FormInput, type FormIssue, type FormOptions, ValidationError, form, forms, invalid, isValidationError };
|
|
286
312
|
//# sourceMappingURL=index.d.mts.map
|
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":";;;;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;;;;;
|
|
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":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -338,6 +338,16 @@ function getFormState(form$1) {
|
|
|
338
338
|
function setFormState(form$1, state) {
|
|
339
339
|
getFormStore().state.set(form$1, state);
|
|
340
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* builds the form action URL, optionally preserving existing search params.
|
|
343
|
+
*/
|
|
344
|
+
function buildActionUrl(configId, preserveParams) {
|
|
345
|
+
if (!preserveParams) return `?__action=${configId}`;
|
|
346
|
+
const context = getContext();
|
|
347
|
+
const url = new URL(context.request.url);
|
|
348
|
+
url.searchParams.set("__action", configId);
|
|
349
|
+
return `?${url.searchParams.toString()}`;
|
|
350
|
+
}
|
|
341
351
|
function form(validateOrFn, maybeFn) {
|
|
342
352
|
const fn = maybeFn ?? validateOrFn;
|
|
343
353
|
const schema = !maybeFn || validateOrFn === "unchecked" ? null : validateOrFn;
|
|
@@ -352,7 +362,7 @@ function form(validateOrFn, maybeFn) {
|
|
|
352
362
|
});
|
|
353
363
|
Object.defineProperty(instance, "action", {
|
|
354
364
|
get() {
|
|
355
|
-
return
|
|
365
|
+
return buildActionUrl(getFormConfig(instance).id, false);
|
|
356
366
|
},
|
|
357
367
|
enumerable: true
|
|
358
368
|
});
|
|
@@ -376,20 +386,37 @@ function form(validateOrFn, maybeFn) {
|
|
|
376
386
|
}
|
|
377
387
|
}, () => getFormState(instance)?.issues ?? {});
|
|
378
388
|
} });
|
|
379
|
-
Object.defineProperty(instance, "buttonProps", { get() {
|
|
380
|
-
return {
|
|
381
|
-
type: "submit",
|
|
382
|
-
formaction: `?__action=${getFormConfig(instance).id}`
|
|
383
|
-
};
|
|
384
|
-
} });
|
|
385
389
|
Object.defineProperty(instance, "__", { value: info });
|
|
386
390
|
Object.defineProperty(instance, kForm, {
|
|
387
391
|
value: true,
|
|
388
392
|
enumerable: false
|
|
389
393
|
});
|
|
394
|
+
Object.defineProperty(instance, "with", { value: (options) => createConfiguredForm(instance, options) });
|
|
390
395
|
return instance;
|
|
391
396
|
}
|
|
392
397
|
/**
|
|
398
|
+
* creates a configured form wrapper with custom options.
|
|
399
|
+
*/
|
|
400
|
+
function createConfiguredForm(source, options) {
|
|
401
|
+
const preserveParams = options.preserveParams ?? false;
|
|
402
|
+
const configured = {};
|
|
403
|
+
Object.defineProperty(configured, "method", {
|
|
404
|
+
value: "POST",
|
|
405
|
+
enumerable: true
|
|
406
|
+
});
|
|
407
|
+
Object.defineProperty(configured, "action", {
|
|
408
|
+
get() {
|
|
409
|
+
return buildActionUrl(getFormConfig(source).id, preserveParams);
|
|
410
|
+
},
|
|
411
|
+
enumerable: true
|
|
412
|
+
});
|
|
413
|
+
Object.defineProperty(configured, "with", { value: (newOptions) => createConfiguredForm(source, {
|
|
414
|
+
...options,
|
|
415
|
+
...newOptions
|
|
416
|
+
}) });
|
|
417
|
+
return configured;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
393
420
|
* redacts sensitive fields (those starting with `_`) from form input.
|
|
394
421
|
* this prevents passwords and other sensitive data from being returned in form state.
|
|
395
422
|
*/
|
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: 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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"}
|
|
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 * options for configuring form behavior.\n */\nexport interface FormOptions {\n\t/**\n\t * if true, preserves existing search params instead of replacing them.\n\t * @default false\n\t */\n\tpreserveParams?: boolean;\n}\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/**\n\t * returns a configured form with the given options.\n\t * @param options form configuration options\n\t * @returns a new form object with the options applied\n\t */\n\twith(options: FormOptions): ConfiguredForm;\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\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 action url helpers\n\n/**\n * builds the form action URL, optionally preserving existing search params.\n */\nfunction buildActionUrl(configId: string, preserveParams: boolean): string {\n\tif (!preserveParams) {\n\t\treturn `?__action=${configId}`;\n\t}\n\n\tconst context = getContext();\n\tconst url = new URL(context.request.url);\n\turl.searchParams.set('__action', configId);\n\treturn `?${url.searchParams.toString()}`;\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, replaces search params by default\n\tObject.defineProperty(instance, 'action', {\n\t\tget() {\n\t\t\tconst config = getFormConfig(instance);\n\t\t\treturn buildActionUrl(config.id, false);\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// 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\t// with() - returns a configured form\n\tObject.defineProperty(instance, 'with', {\n\t\tvalue: (options: FormOptions) => createConfiguredForm(instance, options),\n\t});\n\n\treturn instance;\n}\n\n/**\n * configured form props for spreading onto a form element.\n */\nexport interface ConfiguredForm {\n\treadonly method: 'POST';\n\treadonly action: string;\n\twith(options: FormOptions): ConfiguredForm;\n}\n\n/**\n * creates a configured form wrapper with custom options.\n */\nfunction createConfiguredForm(source: InternalForm<any, any>, options: FormOptions): ConfiguredForm {\n\tconst preserveParams = options.preserveParams ?? false;\n\tconst configured = {} as ConfiguredForm;\n\n\tObject.defineProperty(configured, 'method', {\n\t\tvalue: 'POST',\n\t\tenumerable: true,\n\t});\n\n\tObject.defineProperty(configured, 'action', {\n\t\tget() {\n\t\t\tconst config = getFormConfig(source);\n\t\t\treturn buildActionUrl(config.id, preserveParams);\n\t\t},\n\t\tenumerable: true,\n\t});\n\n\tObject.defineProperty(configured, 'with', {\n\t\tvalue: (newOptions: FormOptions) => createConfiguredForm(source, { ...options, ...newOptions }),\n\t});\n\n\treturn configured;\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
package/src/index.ts
CHANGED
|
@@ -1,5 +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 } from './lib/form.ts';
|
|
4
|
+
export type { Form, FormOptions, ConfiguredForm } from './lib/form.ts';
|
|
5
5
|
export type { FormInput, FormIssue } from './lib/types.ts';
|
package/src/lib/form.ts
CHANGED
|
@@ -86,6 +86,17 @@ export interface FormStore {
|
|
|
86
86
|
*/
|
|
87
87
|
export const FORM_STORE_KEY = createInjectionKey<FormStore>();
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* options for configuring form behavior.
|
|
91
|
+
*/
|
|
92
|
+
export interface FormOptions {
|
|
93
|
+
/**
|
|
94
|
+
* if true, preserves existing search params instead of replacing them.
|
|
95
|
+
* @default false
|
|
96
|
+
*/
|
|
97
|
+
preserveParams?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
/**
|
|
90
101
|
* the return value of a form() function.
|
|
91
102
|
* can be spread onto a <form> element.
|
|
@@ -99,8 +110,12 @@ export interface Form<Input extends FormInput | void, Output> {
|
|
|
99
110
|
readonly result: Output | undefined;
|
|
100
111
|
/** access form fields using object notation */
|
|
101
112
|
readonly fields: FormFields<Input>;
|
|
102
|
-
/**
|
|
103
|
-
|
|
113
|
+
/**
|
|
114
|
+
* returns a configured form with the given options.
|
|
115
|
+
* @param options form configuration options
|
|
116
|
+
* @returns a new form object with the options applied
|
|
117
|
+
*/
|
|
118
|
+
with(options: FormOptions): ConfiguredForm;
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
/**
|
|
@@ -112,11 +127,6 @@ export interface InternalForm<Input extends FormInput | void, Output> extends Fo
|
|
|
112
127
|
readonly __: FormInfo;
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
export interface FormButtonProps {
|
|
116
|
-
type: 'submit';
|
|
117
|
-
readonly formaction: string;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
130
|
// #region issue creator
|
|
121
131
|
|
|
122
132
|
/**
|
|
@@ -213,6 +223,24 @@ export function setFormState<Input, Output>(
|
|
|
213
223
|
|
|
214
224
|
// #endregion
|
|
215
225
|
|
|
226
|
+
// #region action url helpers
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* builds the form action URL, optionally preserving existing search params.
|
|
230
|
+
*/
|
|
231
|
+
function buildActionUrl(configId: string, preserveParams: boolean): string {
|
|
232
|
+
if (!preserveParams) {
|
|
233
|
+
return `?__action=${configId}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const context = getContext();
|
|
237
|
+
const url = new URL(context.request.url);
|
|
238
|
+
url.searchParams.set('__action', configId);
|
|
239
|
+
return `?${url.searchParams.toString()}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// #endregion
|
|
243
|
+
|
|
216
244
|
// #region form function
|
|
217
245
|
|
|
218
246
|
/**
|
|
@@ -261,11 +289,11 @@ export function form(
|
|
|
261
289
|
enumerable: true,
|
|
262
290
|
});
|
|
263
291
|
|
|
264
|
-
// action - computed from form store
|
|
292
|
+
// action - computed from form store, replaces search params by default
|
|
265
293
|
Object.defineProperty(instance, 'action', {
|
|
266
294
|
get() {
|
|
267
295
|
const config = getFormConfig(instance);
|
|
268
|
-
return
|
|
296
|
+
return buildActionUrl(config.id, false);
|
|
269
297
|
},
|
|
270
298
|
enumerable: true,
|
|
271
299
|
});
|
|
@@ -298,17 +326,6 @@ export function form(
|
|
|
298
326
|
},
|
|
299
327
|
});
|
|
300
328
|
|
|
301
|
-
// buttonProps
|
|
302
|
-
Object.defineProperty(instance, 'buttonProps', {
|
|
303
|
-
get() {
|
|
304
|
-
const config = getFormConfig(instance);
|
|
305
|
-
return {
|
|
306
|
-
type: 'submit' as const,
|
|
307
|
-
formaction: `?__action=${config.id}`,
|
|
308
|
-
};
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
|
|
312
329
|
// internal info
|
|
313
330
|
Object.defineProperty(instance, '__', {
|
|
314
331
|
value: info,
|
|
@@ -320,9 +337,50 @@ export function form(
|
|
|
320
337
|
enumerable: false,
|
|
321
338
|
});
|
|
322
339
|
|
|
340
|
+
// with() - returns a configured form
|
|
341
|
+
Object.defineProperty(instance, 'with', {
|
|
342
|
+
value: (options: FormOptions) => createConfiguredForm(instance, options),
|
|
343
|
+
});
|
|
344
|
+
|
|
323
345
|
return instance;
|
|
324
346
|
}
|
|
325
347
|
|
|
348
|
+
/**
|
|
349
|
+
* configured form props for spreading onto a form element.
|
|
350
|
+
*/
|
|
351
|
+
export interface ConfiguredForm {
|
|
352
|
+
readonly method: 'POST';
|
|
353
|
+
readonly action: string;
|
|
354
|
+
with(options: FormOptions): ConfiguredForm;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* creates a configured form wrapper with custom options.
|
|
359
|
+
*/
|
|
360
|
+
function createConfiguredForm(source: InternalForm<any, any>, options: FormOptions): ConfiguredForm {
|
|
361
|
+
const preserveParams = options.preserveParams ?? false;
|
|
362
|
+
const configured = {} as ConfiguredForm;
|
|
363
|
+
|
|
364
|
+
Object.defineProperty(configured, 'method', {
|
|
365
|
+
value: 'POST',
|
|
366
|
+
enumerable: true,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
Object.defineProperty(configured, 'action', {
|
|
370
|
+
get() {
|
|
371
|
+
const config = getFormConfig(source);
|
|
372
|
+
return buildActionUrl(config.id, preserveParams);
|
|
373
|
+
},
|
|
374
|
+
enumerable: true,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
Object.defineProperty(configured, 'with', {
|
|
378
|
+
value: (newOptions: FormOptions) => createConfiguredForm(source, { ...options, ...newOptions }),
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
return configured;
|
|
382
|
+
}
|
|
383
|
+
|
|
326
384
|
// #endregion
|
|
327
385
|
|
|
328
386
|
// #region form processing
|
package/src/lib/types.ts
CHANGED
|
@@ -85,15 +85,20 @@ export type FieldInputType<T> = {
|
|
|
85
85
|
}[keyof InputTypeMap];
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
*
|
|
88
|
+
* value argument for the `as()` method based on input type.
|
|
89
|
+
* returns `[value]` tuple for types that require a value, or `[]` otherwise.
|
|
90
|
+
*
|
|
91
|
+
* note: separating `type` from `value` in the `.as()` signature ensures TypeScript
|
|
92
|
+
* can properly infer the type parameter before resolving the value constraint.
|
|
93
|
+
* using `...args: [type, value?]` with a union of tuple types causes inference issues.
|
|
89
94
|
*/
|
|
90
|
-
export type
|
|
95
|
+
export type AsValueArgs<Type extends keyof InputTypeMap, Value> = Type extends 'checkbox'
|
|
91
96
|
? Value extends string[]
|
|
92
|
-
? [
|
|
93
|
-
: [
|
|
97
|
+
? [value: Value[number] | (string & {})]
|
|
98
|
+
: []
|
|
94
99
|
: Type extends 'radio' | 'submit' | 'hidden'
|
|
95
|
-
? [
|
|
96
|
-
: [
|
|
100
|
+
? [value: Value | (string & {})]
|
|
101
|
+
: [];
|
|
97
102
|
|
|
98
103
|
// #endregion
|
|
99
104
|
|
|
@@ -200,7 +205,7 @@ export type FormFieldLeaf<T extends FormFieldValue> = FieldMethods<T> & {
|
|
|
200
205
|
* <input {...fields.color.as('radio', 'red')} />
|
|
201
206
|
* ```
|
|
202
207
|
*/
|
|
203
|
-
as<K extends FieldInputType<T>>(...
|
|
208
|
+
as<K extends FieldInputType<T>>(type: K, ...value: AsValueArgs<K, T>): InputElementProps<K>;
|
|
204
209
|
};
|
|
205
210
|
|
|
206
211
|
/**
|
|
@@ -214,28 +219,35 @@ export type FormFieldContainer<T> = FieldMethods<T> & {
|
|
|
214
219
|
/**
|
|
215
220
|
* fallback field type when recursion would be infinite
|
|
216
221
|
*/
|
|
217
|
-
type
|
|
222
|
+
type UnknownField<T> = FieldMethods<T> & {
|
|
218
223
|
/** get all issues for this field and descendants */
|
|
219
224
|
allIssues(): FormIssue[] | undefined;
|
|
220
225
|
/** get props for an input element */
|
|
221
|
-
as<K extends FieldInputType<FormFieldValue>>(
|
|
226
|
+
as<K extends FieldInputType<FormFieldValue>>(
|
|
227
|
+
type: K,
|
|
228
|
+
...value: AsValueArgs<K, FormFieldValue>
|
|
229
|
+
): InputElementProps<K>;
|
|
222
230
|
} & {
|
|
223
|
-
[key: string | number]:
|
|
231
|
+
[key: string | number]: UnknownField<any>;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// by breaking this out into its own type, we avoid the TS recursion depth limit
|
|
235
|
+
type RecursiveFormFields = FormFieldContainer<any> & {
|
|
236
|
+
[key: string | number]: UnknownField<any>;
|
|
224
237
|
};
|
|
225
238
|
|
|
226
239
|
/**
|
|
227
240
|
* recursive type to build form fields structure with proxy access.
|
|
228
241
|
* preserves type information through the object hierarchy.
|
|
229
242
|
*/
|
|
230
|
-
export type FormFields<T> =
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
? FormFieldUnknown<T>
|
|
243
|
+
export type FormFields<T> =
|
|
244
|
+
WillRecurseIndefinitely<T> extends true
|
|
245
|
+
? RecursiveFormFields
|
|
234
246
|
: NonNullable<T> extends string | number | boolean | File
|
|
235
247
|
? FormFieldLeaf<NonNullable<T>>
|
|
236
|
-
: T extends string[] | File[]
|
|
248
|
+
: [T] extends [string[] | File[]]
|
|
237
249
|
? FormFieldLeaf<T> & { [K in number]: FormFieldLeaf<T[number]> }
|
|
238
|
-
: T extends Array<infer U>
|
|
250
|
+
: [T] extends [Array<infer U>]
|
|
239
251
|
? FormFieldContainer<T> & { [K in number]: FormFields<U> }
|
|
240
252
|
: FormFieldContainer<T> & { [K in keyof T]-?: FormFields<T[K]> };
|
|
241
253
|
|