@oomfware/forms 0.1.0 → 0.2.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 +3 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +17 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/lib/form.ts +11 -2
- package/src/lib/middleware.ts +20 -6
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
|
|
@@ -33,8 +33,7 @@ type InvalidFieldArray<T> = {
|
|
|
33
33
|
[index: number]: T extends object ? InvalidField<T> : (message: string) => StandardSchemaV1.Issue;
|
|
34
34
|
} & ((message: string) => StandardSchemaV1.Issue);
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
* used by the forms() middleware to identify and process forms.
|
|
36
|
+
* symbol used to identify form instances.
|
|
38
37
|
*/
|
|
39
38
|
|
|
40
39
|
/**
|
|
@@ -139,7 +138,7 @@ type FormDefinitions = Record<string, Form<any, any>>;
|
|
|
139
138
|
* });
|
|
140
139
|
* ```
|
|
141
140
|
*/
|
|
142
|
-
declare function forms(definitions: FormDefinitions):
|
|
141
|
+
declare function forms(definitions: FormDefinitions): Middleware;
|
|
143
142
|
//#endregion
|
|
144
143
|
//#region src/lib/errors.d.ts
|
|
145
144
|
/**
|
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":";;;;KAAY,kBAAkB,IAAI,QAAQ;;;UCgBzB,SAAA;EDhBL,CAAA,GAAA,EAAA,MAAA,CAAA,ECiBI,UDjBQ,CAAA,MAAA,GAAA,MAAA,GAAA,OAAA,GCiB+B,IDjB/B,GCiBsC,SDjBtC,CAAA;;KCoBnB,UDpBqC,CAAA,CAAA,CAAA,GCoBrB,CDpBqB,GCoBjB,CDpBiB,EAAA;AAAR,UCsBjB,SAAA,CDtBiB;EAAO,OAAA,EAAA,MAAA;;;;ACgBzC;;;;;AAEC;AAID;AAoBA;;;;;;;AAGI,KAHQ,YAGR,CAAA,CAAA,CAAA,GAAA,CAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAHgD,gBAAA,CAAiB,KAGjE,CAAA,GAAA,QAAE,MAFO,CAEP,KAFa,CAEb,CAFe,CAEf,CAAA,SAAA,CAAA,KAAA,EAAA,CAAA,EAAA,GADF,iBACE,CADgB,CAChB,CAAA,GAAF,CAAE,CAAA,CAAA,CAAA,SAAA,MAAA,GACD,YADC,CACY,CADZ,CACc,CADd,CAAA,CAAA,GAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAEoB,gBAAA,CAAiB,KAFrC,EACY;KAIb,iBAJe,CAAA,CAAA,CAAA,GAAA;EAAf,CAAA,KAAA,EAAA,MAAA,CAAA,EAKa,CALb,SAAA,MAAA,GAKgC,YALhC,CAK6C,CAL7C,CAAA,GAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAKuE,gBAAA,CAAiB,KALxF;CACqB,GAAA,CAAA,CAAA,OAAA,EAAA,MAAiB,EAAA,GAKjB,gBAAA,CAAiB,KALA,CAAA;;AACzC
|
|
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
|
@@ -277,6 +277,10 @@ function createFieldProxy(target, getInput, setInput, getIssues, path = []) {
|
|
|
277
277
|
//#endregion
|
|
278
278
|
//#region src/lib/form.ts
|
|
279
279
|
/**
|
|
280
|
+
* symbol used to identify form instances.
|
|
281
|
+
*/
|
|
282
|
+
const kForm = Symbol.for("@oomfware/forms");
|
|
283
|
+
/**
|
|
280
284
|
* injection key for the form store.
|
|
281
285
|
*/
|
|
282
286
|
const FORM_STORE_KEY = createInjectionKey();
|
|
@@ -339,7 +343,6 @@ function form(validateOrFn, maybeFn) {
|
|
|
339
343
|
const schema = !maybeFn || validateOrFn === "unchecked" ? null : validateOrFn;
|
|
340
344
|
const instance = {};
|
|
341
345
|
const info = {
|
|
342
|
-
type: "form",
|
|
343
346
|
schema,
|
|
344
347
|
fn
|
|
345
348
|
};
|
|
@@ -380,6 +383,10 @@ function form(validateOrFn, maybeFn) {
|
|
|
380
383
|
};
|
|
381
384
|
} });
|
|
382
385
|
Object.defineProperty(instance, "__", { value: info });
|
|
386
|
+
Object.defineProperty(instance, kForm, {
|
|
387
|
+
value: true,
|
|
388
|
+
enumerable: false
|
|
389
|
+
});
|
|
383
390
|
return instance;
|
|
384
391
|
}
|
|
385
392
|
/**
|
|
@@ -433,6 +440,12 @@ async function processForm(formInstance, data) {
|
|
|
433
440
|
//#endregion
|
|
434
441
|
//#region src/lib/middleware.ts
|
|
435
442
|
/**
|
|
443
|
+
* checks if a value is a form instance created by form().
|
|
444
|
+
*/
|
|
445
|
+
function isForm(value) {
|
|
446
|
+
return value !== null && typeof value === "object" && kForm in value;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
436
449
|
* creates a forms middleware that registers forms and handles form submissions.
|
|
437
450
|
*
|
|
438
451
|
* @example
|
|
@@ -463,12 +476,12 @@ function forms(definitions) {
|
|
|
463
476
|
const formConfig = /* @__PURE__ */ new WeakMap();
|
|
464
477
|
const formsById = /* @__PURE__ */ new Map();
|
|
465
478
|
for (const [name, formInstance] of Object.entries(definitions)) {
|
|
479
|
+
if (!isForm(formInstance)) continue;
|
|
466
480
|
const f = formInstance;
|
|
467
481
|
formConfig.set(f, { id: name });
|
|
468
482
|
formsById.set(name, f);
|
|
469
483
|
}
|
|
470
|
-
return async (
|
|
471
|
-
const { url, request, store } = context;
|
|
484
|
+
return async ({ request, url, store }, next) => {
|
|
472
485
|
const formStore = {
|
|
473
486
|
configs: formConfig,
|
|
474
487
|
state: /* @__PURE__ */ new WeakMap()
|
|
@@ -479,7 +492,7 @@ function forms(definitions) {
|
|
|
479
492
|
const formInstance = formsById.get(action);
|
|
480
493
|
if (formInstance) setFormState(formInstance, await processForm(formInstance, convertFormData(await request.formData())));
|
|
481
494
|
}
|
|
482
|
-
return next(
|
|
495
|
+
return next();
|
|
483
496
|
};
|
|
484
497
|
}
|
|
485
498
|
|
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 * internal info attached to a form instance.\n * used by the forms() middleware to identify and process forms.\n */\nexport interface FormInfo {\n\ttype: 'form';\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\ttype: 'form',\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\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\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 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\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":"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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: 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 { 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// #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// 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.0",
|
|
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/lib/form.ts
CHANGED
|
@@ -52,12 +52,16 @@ type InvalidFieldArray<T> = {
|
|
|
52
52
|
[index: number]: T extends object ? InvalidField<T> : (message: string) => StandardSchemaV1.Issue;
|
|
53
53
|
} & ((message: string) => StandardSchemaV1.Issue);
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* symbol used to identify form instances.
|
|
57
|
+
*/
|
|
58
|
+
export const kForm = Symbol.for('@oomfware/forms');
|
|
59
|
+
|
|
55
60
|
/**
|
|
56
61
|
* internal info attached to a form instance.
|
|
57
62
|
* used by the forms() middleware to identify and process forms.
|
|
58
63
|
*/
|
|
59
64
|
export interface FormInfo {
|
|
60
|
-
type: 'form';
|
|
61
65
|
/** the schema, if any */
|
|
62
66
|
schema: StandardSchemaV1 | null;
|
|
63
67
|
/** the handler function */
|
|
@@ -322,7 +326,6 @@ export function form(
|
|
|
322
326
|
const instance = {} as InternalForm<any, any>;
|
|
323
327
|
|
|
324
328
|
const info: FormInfo = {
|
|
325
|
-
type: 'form',
|
|
326
329
|
schema,
|
|
327
330
|
fn,
|
|
328
331
|
};
|
|
@@ -386,6 +389,12 @@ export function form(
|
|
|
386
389
|
value: info,
|
|
387
390
|
});
|
|
388
391
|
|
|
392
|
+
// brand symbol for identification
|
|
393
|
+
Object.defineProperty(instance, kForm, {
|
|
394
|
+
value: true,
|
|
395
|
+
enumerable: false,
|
|
396
|
+
});
|
|
397
|
+
|
|
389
398
|
return instance;
|
|
390
399
|
}
|
|
391
400
|
|
package/src/lib/middleware.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Middleware } from '@oomfware/fetch-router';
|
|
2
2
|
|
|
3
3
|
import { convertFormData } from './form-utils.ts';
|
|
4
4
|
import {
|
|
5
5
|
FORM_STORE_KEY,
|
|
6
|
+
kForm,
|
|
6
7
|
processForm,
|
|
7
8
|
setFormState,
|
|
8
9
|
type Form,
|
|
@@ -20,6 +21,17 @@ export type FormDefinitions = Record<string, Form<any, any>>;
|
|
|
20
21
|
|
|
21
22
|
// #endregion
|
|
22
23
|
|
|
24
|
+
// #region helpers
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* checks if a value is a form instance created by form().
|
|
28
|
+
*/
|
|
29
|
+
function isForm(value: unknown): value is Form<any, any> {
|
|
30
|
+
return value !== null && typeof value === 'object' && kForm in value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// #endregion
|
|
34
|
+
|
|
23
35
|
// #region middleware
|
|
24
36
|
|
|
25
37
|
/**
|
|
@@ -49,20 +61,22 @@ export type FormDefinitions = Record<string, Form<any, any>>;
|
|
|
49
61
|
* });
|
|
50
62
|
* ```
|
|
51
63
|
*/
|
|
52
|
-
export function forms(definitions: FormDefinitions):
|
|
64
|
+
export function forms(definitions: FormDefinitions): Middleware {
|
|
53
65
|
const formConfig = new WeakMap<InternalForm<any, any>, FormConfig>();
|
|
54
66
|
const formsById = new Map<string, InternalForm<any, any>>();
|
|
55
67
|
|
|
56
68
|
for (const [name, formInstance] of Object.entries(definitions)) {
|
|
69
|
+
if (!isForm(formInstance)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
const f = formInstance as InternalForm<any, any>;
|
|
58
74
|
|
|
59
75
|
formConfig.set(f, { id: name });
|
|
60
76
|
formsById.set(name, f);
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
return async (
|
|
64
|
-
const { url, request, store } = context;
|
|
65
|
-
|
|
79
|
+
return async ({ request, url, store }, next) => {
|
|
66
80
|
// create form store for this request
|
|
67
81
|
const formStore: FormStore = {
|
|
68
82
|
configs: formConfig,
|
|
@@ -92,7 +106,7 @@ export function forms(definitions: FormDefinitions): RouterMiddleware {
|
|
|
92
106
|
}
|
|
93
107
|
}
|
|
94
108
|
|
|
95
|
-
return next(
|
|
109
|
+
return next();
|
|
96
110
|
};
|
|
97
111
|
}
|
|
98
112
|
|