@zerolith/forms 0.0.0-canary.20261102.g8292c77
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/LICENSE +201 -0
- package/README.md +32 -0
- package/dist/form.d.ts +91 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +246 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/problem.d.ts +11 -0
- package/dist/problem.d.ts.map +1 -0
- package/dist/problem.js +17 -0
- package/dist/problem.js.map +1 -0
- package/dist/schema.d.ts +62 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +55 -0
- package/dist/schema.js.map +1 -0
- package/package.json +50 -0
- package/src/form.ts +397 -0
- package/src/index.ts +11 -0
- package/src/problem.ts +18 -0
- package/src/schema.ts +122 -0
package/src/schema.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation through Standard Schema, the interface that zod, valibot,
|
|
3
|
+
* arktype, and others implement, so a schema written once validates a
|
|
4
|
+
* submission in the browser before it is sent and on the server when it
|
|
5
|
+
* arrives, producing the same problem document in both places.
|
|
6
|
+
*/
|
|
7
|
+
import type { FieldErrors, Problem } from "zerolith";
|
|
8
|
+
|
|
9
|
+
/** One segment of an issue path in object form. */
|
|
10
|
+
export interface StandardPathSegment
|
|
11
|
+
{
|
|
12
|
+
/** The key representing the path segment; `problemFromIssues` converts it to a string and joins the path's keys with dots to name the field. */
|
|
13
|
+
readonly key: PropertyKey;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One validation failure. */
|
|
17
|
+
export interface StandardIssue
|
|
18
|
+
{
|
|
19
|
+
/** The error message of the issue; `problemFromIssues` lists it under the field its path names, or in `detail` when the path is absent or empty. */
|
|
20
|
+
readonly message: string;
|
|
21
|
+
/** The path to the failing value; absent for an issue about the whole input. */
|
|
22
|
+
readonly path?: ReadonlyArray<PropertyKey | StandardPathSegment> | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What `validate` resolves to: a value, or issues. */
|
|
26
|
+
export type StandardResult<Output> =
|
|
27
|
+
| { readonly value: Output; readonly issues?: undefined }
|
|
28
|
+
| { readonly issues: ReadonlyArray<StandardIssue> };
|
|
29
|
+
|
|
30
|
+
/** The Standard Schema V1 interface, as published at standardschema.dev. */
|
|
31
|
+
export interface StandardSchema<Input = unknown, Output = Input>
|
|
32
|
+
{
|
|
33
|
+
/** The Standard properties: the `version` of the specification, the `vendor` name of the schema library, the optional inferred `types`, and the `validate` function that `validateForm` calls and awaits. */
|
|
34
|
+
readonly "~standard": {
|
|
35
|
+
readonly version: 1;
|
|
36
|
+
readonly vendor: string;
|
|
37
|
+
readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
|
|
38
|
+
readonly types?: { readonly input: Input; readonly output: Output } | undefined;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The output type of a schema. */
|
|
43
|
+
export type InferOutput<S extends StandardSchema> = S extends StandardSchema<unknown, infer Output> ? Output : never;
|
|
44
|
+
|
|
45
|
+
/** A form's values as a plain object: repeated names become arrays. */
|
|
46
|
+
export type FormValues = Record<string, FormDataEntryValue | FormDataEntryValue[]>;
|
|
47
|
+
|
|
48
|
+
/** What `validateForm` resolves to: the parsed value, or the problem to show. */
|
|
49
|
+
export type Validation<Output> =
|
|
50
|
+
| { readonly value: Output; readonly problem?: undefined }
|
|
51
|
+
| { readonly value?: undefined; readonly problem: Problem };
|
|
52
|
+
|
|
53
|
+
/** Turns `FormData` into a plain object with no prototype, so a field named `constructor` or `__proto__` is a field like any other. A name that appears once maps to its value; a name that appears more than once maps to an array of its values, in order. */
|
|
54
|
+
export function formValues(data: FormData): FormValues
|
|
55
|
+
{
|
|
56
|
+
const values = Object.create(null) as FormValues;
|
|
57
|
+
for (const [name, value] of data)
|
|
58
|
+
{
|
|
59
|
+
const current = values[name];
|
|
60
|
+
if (current === undefined)
|
|
61
|
+
{
|
|
62
|
+
values[name] = value;
|
|
63
|
+
}
|
|
64
|
+
else if (Array.isArray(current))
|
|
65
|
+
{
|
|
66
|
+
current.push(value);
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
{
|
|
70
|
+
values[name] = [current, value];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return values;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fieldName(path: ReadonlyArray<PropertyKey | StandardPathSegment> | undefined): string | undefined
|
|
77
|
+
{
|
|
78
|
+
if (path === undefined || path.length === 0)
|
|
79
|
+
{
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return path.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Groups issues into a problem: path-bearing issues under `fields`, the rest in `detail`. */
|
|
86
|
+
export function problemFromIssues(issues: ReadonlyArray<StandardIssue>): Problem
|
|
87
|
+
{
|
|
88
|
+
const fields = Object.create(null) as Record<string, string[]>;
|
|
89
|
+
const general: string[] = [];
|
|
90
|
+
for (const issue of issues)
|
|
91
|
+
{
|
|
92
|
+
const name = fieldName(issue.path);
|
|
93
|
+
if (name === undefined)
|
|
94
|
+
{
|
|
95
|
+
general.push(issue.message);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
(fields[name] ??= []).push(issue.message);
|
|
99
|
+
}
|
|
100
|
+
const problem: Problem = { type: "about:blank", title: "The submission has invalid fields", status: 400, fields: fields as FieldErrors };
|
|
101
|
+
if (general.length > 0)
|
|
102
|
+
{
|
|
103
|
+
problem.detail = general.join(" ");
|
|
104
|
+
}
|
|
105
|
+
return problem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validates a submission with a Standard Schema. `FormData` is converted
|
|
110
|
+
* with `formValues` first; any other value is validated as it is. Issues
|
|
111
|
+
* become a problem whose `fields` carry the messages per path.
|
|
112
|
+
*/
|
|
113
|
+
export async function validateForm<S extends StandardSchema>(schema: S, data: FormData | Record<string, unknown>): Promise<Validation<InferOutput<S>>>
|
|
114
|
+
{
|
|
115
|
+
const input = data instanceof FormData ? formValues(data) : data;
|
|
116
|
+
const result = await schema["~standard"].validate(input);
|
|
117
|
+
if (result.issues !== undefined && result.issues.length > 0)
|
|
118
|
+
{
|
|
119
|
+
return { problem: problemFromIssues(result.issues) };
|
|
120
|
+
}
|
|
121
|
+
return { value: (result as { value: InferOutput<S> }).value };
|
|
122
|
+
}
|