@formadapter/server 0.0.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/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +350 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ludicrous
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# `@formadapter/server`
|
|
2
|
+
|
|
3
|
+
Framework-neutral schema-aware FormData/JSON validation and reusable submission
|
|
4
|
+
transports for FormAdapter.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
bun add @formadapter/server
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The package compiles a schema's input-side Standard JSON Schema once and
|
|
11
|
+
delegates authoritative validation/transformation to the original Standard
|
|
12
|
+
Schema. FormData is decoded from model-known paths and normalized for browser
|
|
13
|
+
semantics first. JSON is passed to the schema exactly as received: unknown
|
|
14
|
+
keys, malformed nested values, and other invalid input are never pruned before
|
|
15
|
+
validation. Zod, ArkType, and any other schema exposing both standards use the
|
|
16
|
+
same boundary.
|
|
17
|
+
|
|
18
|
+
## Reusable submissions
|
|
19
|
+
|
|
20
|
+
Build one transport-neutral submission, then expose it through any boundary:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import {
|
|
25
|
+
createSubmissionHandler,
|
|
26
|
+
fieldError,
|
|
27
|
+
toRequestHandler,
|
|
28
|
+
toServerAction,
|
|
29
|
+
} from "@formadapter/server";
|
|
30
|
+
|
|
31
|
+
const schema = z.object({
|
|
32
|
+
email: z.email(),
|
|
33
|
+
age: z.number().int().min(18),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const submission = createSubmissionHandler(schema, async (values) => {
|
|
37
|
+
if (await emailAlreadyExists(values.email)) {
|
|
38
|
+
throw fieldError("email", "That email is already registered");
|
|
39
|
+
}
|
|
40
|
+
return database.users.create({ data: values });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const action = toServerAction(submission);
|
|
44
|
+
export const POST = toRequestHandler(submission);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`createServerAction(schema, handler)` and
|
|
48
|
+
`createRequestHandler(schema, handler)` are direct convenience factories. A
|
|
49
|
+
server action has React's native `(previousState, FormData)` signature. The HTTP
|
|
50
|
+
handler accepts POST requests with JSON, `multipart/form-data`, or
|
|
51
|
+
`application/x-www-form-urlencoded` bodies and returns serialized
|
|
52
|
+
`SubmissionState` JSON.
|
|
53
|
+
|
|
54
|
+
Pass the same `config` used to create the client form when server parsing must
|
|
55
|
+
apply `requiredWhenVisible` rules. Model-decoded FormData also prunes conditional
|
|
56
|
+
`hidden` branches. JSON stays exactly as received so strict-object and malformed
|
|
57
|
+
payload errors cannot disappear before schema validation:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const submission = createSubmissionHandler(schema, save, { config: formConfig });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Expected business failures should be thrown with `formError`, `fieldError`, or
|
|
64
|
+
`FormAdapterServerError`. They become structured field/form errors. Unexpected
|
|
65
|
+
errors are deliberately rethrown so framework error boundaries, monitoring,
|
|
66
|
+
redirects, and not-found control flow are not swallowed.
|
|
67
|
+
|
|
68
|
+
Transport adapters may add context to the submission's second argument. For
|
|
69
|
+
example, the TanStack Start adapter preserves its middleware `context`, method,
|
|
70
|
+
and server-function metadata for the business handler. Adapter-owned
|
|
71
|
+
`payloadKind` and `formData` values cannot be overridden by transport input.
|
|
72
|
+
|
|
73
|
+
Use `createSubmissionHandlerFactory<InvocationContext>()` to bind a transport
|
|
74
|
+
context once while retaining schema-output and handler-result inference. Its
|
|
75
|
+
returned submissions require the context argument; the default
|
|
76
|
+
`createSubmissionHandler` remains optional-context and backward compatible.
|
|
77
|
+
|
|
78
|
+
## Typed FormData decoding
|
|
79
|
+
|
|
80
|
+
The React runtime uses dotted paths for objects and array indices. Three
|
|
81
|
+
repeatable hidden markers preserve information native FormData normally loses:
|
|
82
|
+
|
|
83
|
+
- `__formadapter_array` records a concrete array path, including an empty array.
|
|
84
|
+
- `__formadapter_boolean` records a boolean path, including an unchecked value.
|
|
85
|
+
- `__formadapter_value` opts a concrete scalar path into tagged string, number,
|
|
86
|
+
boolean, or null decoding.
|
|
87
|
+
|
|
88
|
+
The exported constants are available for custom renderers. `parseFormData`
|
|
89
|
+
ignores framework `$ACTION_` entries and unknown fields, protects unsafe object
|
|
90
|
+
path segments and oversized indices, preserves files, normalizes optional and
|
|
91
|
+
nullable blanks, and prunes presentation-hidden branches before validation.
|
|
92
|
+
|
|
93
|
+
FormData parsing throws a descriptive configuration error for schema paths it
|
|
94
|
+
cannot encode: FormAdapter's `__formadapter_` namespace, `$ACTION_` names,
|
|
95
|
+
prototype keys, numeric-like keys, and keys containing path syntax such as dots,
|
|
96
|
+
brackets, or quotes. JSON-only server submissions can still validate those
|
|
97
|
+
properties, but FormAdapter cannot render or encode them as form fields.
|
|
98
|
+
|
|
99
|
+
Do not treat hidden inputs as trusted data. Authentication, authorization, and
|
|
100
|
+
ownership checks remain the responsibility of every server handler.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { FormConfig, FormSchema, InferInput, InferOutput, MaybePromise, SubmissionAction, SubmissionState } from "@formadapter/core";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
interface ParseFormDataSuccess<Output> {
|
|
5
|
+
readonly success: true;
|
|
6
|
+
readonly data: Output;
|
|
7
|
+
}
|
|
8
|
+
interface ParseFormDataFailure {
|
|
9
|
+
readonly success: false;
|
|
10
|
+
readonly fieldErrors: Readonly<Record<string, readonly string[]>>;
|
|
11
|
+
readonly formErrors: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
type ParseFormDataResult<Output> = ParseFormDataFailure | ParseFormDataSuccess<Output>;
|
|
14
|
+
interface SubmissionOptions<Schema extends FormSchema> {
|
|
15
|
+
readonly config?: FormConfig<InferInput<Schema>, string>;
|
|
16
|
+
}
|
|
17
|
+
interface SubmissionInvocationContext<Data = unknown> {
|
|
18
|
+
/** Transport-specific values (for example TanStack Start middleware context). */
|
|
19
|
+
readonly [key: string]: unknown;
|
|
20
|
+
readonly previousState?: SubmissionState<Data>;
|
|
21
|
+
readonly request?: Request;
|
|
22
|
+
}
|
|
23
|
+
type SubmissionHandlerContext<Data = unknown, InvocationContext extends object = SubmissionInvocationContext<Data>> = InvocationContext & {
|
|
24
|
+
readonly payloadKind: "form-data" | "json";
|
|
25
|
+
readonly formData?: FormData;
|
|
26
|
+
};
|
|
27
|
+
type SubmissionHandlerResult<Data> = Data | SubmissionState<Data> | void;
|
|
28
|
+
type SubmissionValueHandler<Schema extends FormSchema, Data = unknown, InvocationContext extends object = SubmissionInvocationContext<Data>> = (value: InferOutput<Schema>, context: SubmissionHandlerContext<Data, InvocationContext>) => MaybePromise<SubmissionHandlerResult<Data>>;
|
|
29
|
+
interface CreatedSubmissionHandler<Data = unknown, InvocationContext extends object = SubmissionInvocationContext<Data>> {
|
|
30
|
+
(payload: unknown, context?: InvocationContext): Promise<SubmissionState<Data>>;
|
|
31
|
+
}
|
|
32
|
+
interface ContextualCreatedSubmissionHandler<Data, InvocationContext extends object> {
|
|
33
|
+
(payload: unknown, context: InvocationContext): Promise<SubmissionState<Data>>;
|
|
34
|
+
}
|
|
35
|
+
type DataFromResolvedHandlerResult<Result> = Result extends SubmissionState<infer Data> ? Data : Result extends void ? unknown : Result;
|
|
36
|
+
type SubmissionDataFromHandlerResult<Result> = DataFromResolvedHandlerResult<Awaited<Result>>;
|
|
37
|
+
interface CreateSubmissionHandler<InvocationContext extends object> {
|
|
38
|
+
<Schema extends FormSchema, Result>(schema: Schema, handler: (value: InferOutput<Schema>, context: SubmissionHandlerContext<unknown, InvocationContext>) => MaybePromise<Result>, options?: SubmissionOptions<Schema>): ContextualCreatedSubmissionHandler<SubmissionDataFromHandlerResult<Result>, InvocationContext>;
|
|
39
|
+
}
|
|
40
|
+
type ServerAction<Data = unknown> = SubmissionAction<FormData, Data>;
|
|
41
|
+
type RequestHandler = (request: Request) => Promise<Response>;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/form-data.d.ts
|
|
44
|
+
declare const FORMADAPTER_ARRAY_MARKER = "__formadapter_array";
|
|
45
|
+
declare const FORMADAPTER_BOOLEAN_MARKER = "__formadapter_boolean";
|
|
46
|
+
declare const FORMADAPTER_VALUE_MARKER = "__formadapter_value";
|
|
47
|
+
/** Decodes and validates FormData using the schema's input-side form model. */
|
|
48
|
+
declare function parseFormData<Schema extends FormSchema>(schema: Schema, formData: FormData, options?: SubmissionOptions<Schema>): Promise<ParseFormDataResult<InferOutput<Schema>>>;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/error.d.ts
|
|
51
|
+
type ErrorMessages = readonly string[] | string;
|
|
52
|
+
interface FormAdapterServerErrorOptions {
|
|
53
|
+
readonly cause?: unknown;
|
|
54
|
+
readonly fieldErrors?: Readonly<Record<string, ErrorMessages>>;
|
|
55
|
+
readonly formErrors?: ErrorMessages;
|
|
56
|
+
readonly message?: string;
|
|
57
|
+
}
|
|
58
|
+
interface ErrorHelperOptions {
|
|
59
|
+
readonly cause?: unknown;
|
|
60
|
+
readonly message?: string;
|
|
61
|
+
}
|
|
62
|
+
/** A deliberate, user-safe business failure raised by a submission handler. */
|
|
63
|
+
declare class FormAdapterServerError extends Error {
|
|
64
|
+
override readonly name = "FormAdapterServerError";
|
|
65
|
+
readonly fieldErrors: Readonly<Record<string, readonly string[]>>;
|
|
66
|
+
readonly formErrors: readonly string[];
|
|
67
|
+
constructor(options?: FormAdapterServerErrorOptions);
|
|
68
|
+
}
|
|
69
|
+
/** Creates a business error intended to be thrown from a submission handler. */
|
|
70
|
+
declare function formError(errorMessages: ErrorMessages, options?: ErrorHelperOptions): FormAdapterServerError;
|
|
71
|
+
/** Creates a field-scoped business error intended to be thrown from a handler. */
|
|
72
|
+
declare function fieldError(path: string, errorMessages: ErrorMessages, options?: ErrorHelperOptions): FormAdapterServerError;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/submission.d.ts
|
|
75
|
+
/** Compiles a schema once and returns a transport-neutral submission function. */
|
|
76
|
+
declare function createSubmissionHandler<Schema extends FormSchema, Data = unknown>(schema: Schema, handler: SubmissionValueHandler<Schema, Data>, options?: SubmissionOptions<Schema>): CreatedSubmissionHandler<Data>;
|
|
77
|
+
declare function createSubmissionHandler<Schema extends FormSchema, Data, InvocationContext extends object>(schema: Schema, handler: SubmissionValueHandler<Schema, Data, InvocationContext>, options?: SubmissionOptions<Schema>): ContextualCreatedSubmissionHandler<Data, InvocationContext>;
|
|
78
|
+
/** Binds a transport invocation-context type while preserving schema/data inference. */
|
|
79
|
+
declare function createSubmissionHandlerFactory<InvocationContext extends object>(): CreateSubmissionHandler<InvocationContext>;
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/transports.d.ts
|
|
82
|
+
/** Adapts a reusable submission to React/Next-style `(state, FormData)` actions. */
|
|
83
|
+
declare function toServerAction<Data>(submission: CreatedSubmissionHandler<Data>): ServerAction<Data>;
|
|
84
|
+
/** Creates a React/Next-style server action directly from a schema and handler. */
|
|
85
|
+
declare function createServerAction<Schema extends FormSchema, Data = unknown>(schema: Schema, handler: SubmissionValueHandler<Schema, Data>, options?: SubmissionOptions<Schema>): ServerAction<Data>;
|
|
86
|
+
/** Adapts a reusable submission to a POST JSON/FormData HTTP request handler. */
|
|
87
|
+
declare function toRequestHandler<Data>(submission: CreatedSubmissionHandler<Data>): RequestHandler;
|
|
88
|
+
/** Creates a JSON/FormData HTTP handler directly from a schema and handler. */
|
|
89
|
+
declare function createRequestHandler<Schema extends FormSchema, Data = unknown>(schema: Schema, handler: SubmissionValueHandler<Schema, Data>, options?: SubmissionOptions<Schema>): RequestHandler;
|
|
90
|
+
//#endregion
|
|
91
|
+
export { type ContextualCreatedSubmissionHandler, type CreateSubmissionHandler, type CreatedSubmissionHandler, type ErrorHelperOptions, type ErrorMessages, FORMADAPTER_ARRAY_MARKER, FORMADAPTER_BOOLEAN_MARKER, FORMADAPTER_VALUE_MARKER, FormAdapterServerError, type FormAdapterServerErrorOptions, type ParseFormDataFailure, type ParseFormDataResult, type ParseFormDataSuccess, type RequestHandler, type ServerAction, type SubmissionDataFromHandlerResult, type SubmissionHandlerContext, type SubmissionHandlerResult, type SubmissionInvocationContext, type SubmissionOptions, type SubmissionValueHandler, createRequestHandler, createServerAction, createSubmissionHandler, createSubmissionHandlerFactory, fieldError, formError, parseFormData, toRequestHandler, toServerAction };
|
|
92
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { compileForm, isReservedFormPathSegment, isSubmissionState, issuePath, optionForSerializedValue, pathToConfigPath, pathToName, prepareFormValues, submissionFailure, submissionSuccess, validatePresentationRules } from "@formadapter/core";
|
|
2
|
+
//#region src/form-data.ts
|
|
3
|
+
const FORMADAPTER_ARRAY_MARKER = "__formadapter_array";
|
|
4
|
+
const FORMADAPTER_BOOLEAN_MARKER = "__formadapter_boolean";
|
|
5
|
+
const FORMADAPTER_VALUE_MARKER = "__formadapter_value";
|
|
6
|
+
const OMIT = Symbol("formadapter.omit");
|
|
7
|
+
const MAX_ARRAY_INDEX = 1e4;
|
|
8
|
+
function assertFormDataFieldNames(model) {
|
|
9
|
+
const reserved = [];
|
|
10
|
+
const visit = (node, root = false) => {
|
|
11
|
+
if (!root && isReservedFormPathSegment(node.key)) reserved.push(node.path || node.key);
|
|
12
|
+
if (node.kind === "object") for (const child of node.children) visit(child);
|
|
13
|
+
else if (node.kind === "array") visit(node.item);
|
|
14
|
+
};
|
|
15
|
+
visit(model.root, true);
|
|
16
|
+
if (reserved.length === 0) return;
|
|
17
|
+
const names = reserved.map((name) => JSON.stringify(name)).join(", ");
|
|
18
|
+
throw new TypeError(`Cannot decode FormData for unrepresentable field path ${names}. Rename the field or use a JSON-only submission.`);
|
|
19
|
+
}
|
|
20
|
+
function own(container, key) {
|
|
21
|
+
return Object.prototype.hasOwnProperty.call(container, key);
|
|
22
|
+
}
|
|
23
|
+
function concretePath(name) {
|
|
24
|
+
if (!name || name.startsWith("$ACTION_")) return void 0;
|
|
25
|
+
const parts = name.split(".");
|
|
26
|
+
const result = [];
|
|
27
|
+
for (const part of parts) if (/^(?:0|[1-9]\d*)$/.test(part)) {
|
|
28
|
+
const index = Number(part);
|
|
29
|
+
if (!Number.isSafeInteger(index) || index > MAX_ARRAY_INDEX) return;
|
|
30
|
+
result.push(index);
|
|
31
|
+
} else {
|
|
32
|
+
if (isReservedFormPathSegment(part)) return void 0;
|
|
33
|
+
result.push(part);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
function nodeAtPath(model, path) {
|
|
38
|
+
return model.fieldMap[pathToConfigPath(path)];
|
|
39
|
+
}
|
|
40
|
+
function createContainer(next) {
|
|
41
|
+
return typeof next === "number" ? [] : {};
|
|
42
|
+
}
|
|
43
|
+
function valueAtPath(root, path) {
|
|
44
|
+
let current = root;
|
|
45
|
+
for (const segment of path) {
|
|
46
|
+
if (typeof current !== "object" || current === null || !own(current, segment)) return;
|
|
47
|
+
current = current[segment];
|
|
48
|
+
}
|
|
49
|
+
return current;
|
|
50
|
+
}
|
|
51
|
+
function hasPath(root, path) {
|
|
52
|
+
let current = root;
|
|
53
|
+
for (const segment of path) {
|
|
54
|
+
if (typeof current !== "object" || current === null || !own(current, segment)) return false;
|
|
55
|
+
current = current[segment];
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function setPath(root, path, value) {
|
|
60
|
+
let current = root;
|
|
61
|
+
for (const [position, segment] of path.entries()) {
|
|
62
|
+
if (position === path.length - 1) {
|
|
63
|
+
current[segment] = value;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const next = path[position + 1];
|
|
67
|
+
const existing = current[segment];
|
|
68
|
+
if (!(typeof existing === "object" && existing !== null && (typeof next === "number" ? Array.isArray(existing) : !Array.isArray(existing)))) current[segment] = createContainer(next);
|
|
69
|
+
current = current[segment];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function appendPath(root, path, value) {
|
|
73
|
+
const existing = valueAtPath(root, path);
|
|
74
|
+
if (Array.isArray(existing)) existing.push(value);
|
|
75
|
+
else setPath(root, path, [value]);
|
|
76
|
+
}
|
|
77
|
+
function isFile(value) {
|
|
78
|
+
return typeof value !== "string";
|
|
79
|
+
}
|
|
80
|
+
function emptyFile(value) {
|
|
81
|
+
return value.name === "" && value.size === 0;
|
|
82
|
+
}
|
|
83
|
+
function serializedOption(value) {
|
|
84
|
+
const separator = value.indexOf(":");
|
|
85
|
+
if (separator === -1) return OMIT;
|
|
86
|
+
const type = value.slice(0, separator);
|
|
87
|
+
const serialized = value.slice(separator + 1);
|
|
88
|
+
switch (type) {
|
|
89
|
+
case "boolean":
|
|
90
|
+
if (serialized === "true") return true;
|
|
91
|
+
if (serialized === "false") return false;
|
|
92
|
+
return OMIT;
|
|
93
|
+
case "null": return serialized === "" ? null : OMIT;
|
|
94
|
+
case "number": {
|
|
95
|
+
const number = Number(serialized);
|
|
96
|
+
return serialized.trim() && Number.isFinite(number) ? number : OMIT;
|
|
97
|
+
}
|
|
98
|
+
case "string": return serialized;
|
|
99
|
+
default: return OMIT;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function hasOptions(node) {
|
|
103
|
+
return Boolean(node.options || node.config.options || node.control === "radio" || node.control === "select");
|
|
104
|
+
}
|
|
105
|
+
function decodeOption(node, value) {
|
|
106
|
+
const staticOption = node.options ? optionForSerializedValue(node.options, value) : void 0;
|
|
107
|
+
if (staticOption) return staticOption.value;
|
|
108
|
+
if (!hasOptions(node)) return OMIT;
|
|
109
|
+
return serializedOption(value);
|
|
110
|
+
}
|
|
111
|
+
function decodeBoolean(value) {
|
|
112
|
+
if (value === "on" || value === "1" || value === "true") return true;
|
|
113
|
+
if (value === "0" || value === "false") return false;
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
function decodeScalar(node, entry, typedValue = false) {
|
|
117
|
+
if (node.dataType === "file") {
|
|
118
|
+
if (!isFile(entry)) return entry;
|
|
119
|
+
if (!emptyFile(entry)) return entry;
|
|
120
|
+
return node.nullable ? null : OMIT;
|
|
121
|
+
}
|
|
122
|
+
if (isFile(entry)) return entry;
|
|
123
|
+
if (typedValue) {
|
|
124
|
+
const typed = serializedOption(entry);
|
|
125
|
+
if (typed !== OMIT) return typed;
|
|
126
|
+
}
|
|
127
|
+
const option = decodeOption(node, entry);
|
|
128
|
+
if (option !== OMIT) return option;
|
|
129
|
+
if (entry === "" && node.nullable) return null;
|
|
130
|
+
if (node.dataType === "number" || node.dataType === "integer") {
|
|
131
|
+
if (!entry.trim()) return OMIT;
|
|
132
|
+
const number = Number(entry);
|
|
133
|
+
return Number.isFinite(number) ? number : entry;
|
|
134
|
+
}
|
|
135
|
+
if (node.dataType === "boolean") return decodeBoolean(entry);
|
|
136
|
+
return entry;
|
|
137
|
+
}
|
|
138
|
+
function markerPaths(formData, marker) {
|
|
139
|
+
return formData.getAll(marker).filter((value) => typeof value === "string").map(concretePath).filter((path) => path !== void 0);
|
|
140
|
+
}
|
|
141
|
+
/** Model-driven decoding. This is intentionally not exported from the package. */
|
|
142
|
+
function decodeFormData(model, formData) {
|
|
143
|
+
const raw = {};
|
|
144
|
+
const typedPaths = new Set(markerPaths(formData, FORMADAPTER_VALUE_MARKER).map(pathToName));
|
|
145
|
+
for (const path of markerPaths(formData, FORMADAPTER_ARRAY_MARKER)) if (nodeAtPath(model, path)?.kind === "array" && !hasPath(raw, path)) setPath(raw, path, []);
|
|
146
|
+
for (const path of markerPaths(formData, FORMADAPTER_BOOLEAN_MARKER)) {
|
|
147
|
+
const node = nodeAtPath(model, path);
|
|
148
|
+
if (node?.kind === "scalar" && node.dataType === "boolean" && !hasPath(raw, path)) setPath(raw, path, false);
|
|
149
|
+
}
|
|
150
|
+
for (const [name, entry] of formData.entries()) {
|
|
151
|
+
if (name === "__formadapter_array" || name === "__formadapter_boolean" || name === "__formadapter_value" || name.startsWith("$ACTION_")) continue;
|
|
152
|
+
const path = concretePath(name);
|
|
153
|
+
if (!path) continue;
|
|
154
|
+
const node = nodeAtPath(model, path);
|
|
155
|
+
if (!node) continue;
|
|
156
|
+
if (node.kind === "scalar") {
|
|
157
|
+
const decoded = decodeScalar(node, entry, typedPaths.has(name));
|
|
158
|
+
if (decoded !== OMIT) setPath(raw, path, decoded);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (node.kind === "array" && node.item.kind === "scalar") {
|
|
162
|
+
const decoded = decodeScalar(node.item, entry, typedPaths.has(name));
|
|
163
|
+
if (decoded !== OMIT) appendPath(raw, path, decoded);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return raw;
|
|
167
|
+
}
|
|
168
|
+
function issuesFailure(issues) {
|
|
169
|
+
const fieldErrors = Object.create(null);
|
|
170
|
+
const formErrors = [];
|
|
171
|
+
for (const issue of issues) {
|
|
172
|
+
const path = pathToName(issuePath(issue).map((segment) => typeof segment === "number" || typeof segment === "string" ? segment : String(segment)));
|
|
173
|
+
if (!path) formErrors.push(issue.message);
|
|
174
|
+
else (fieldErrors[path] ??= []).push(issue.message);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
fieldErrors,
|
|
179
|
+
formErrors
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function validateSubmissionValue(schema, model, value) {
|
|
183
|
+
const presentationIssues = validatePresentationRules(model, value);
|
|
184
|
+
const result = await schema["~standard"].validate(value);
|
|
185
|
+
if (result.issues !== void 0) return issuesFailure([...result.issues.length > 0 ? result.issues : [{ message: "Schema validation failed" }], ...presentationIssues]);
|
|
186
|
+
return presentationIssues.length > 0 ? issuesFailure(presentationIssues) : {
|
|
187
|
+
success: true,
|
|
188
|
+
data: result.value
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function parseFormDataWithModel(schema, model, formData) {
|
|
192
|
+
assertFormDataFieldNames(model);
|
|
193
|
+
return validateSubmissionValue(schema, model, prepareFormValues(model, decodeFormData(model, formData)));
|
|
194
|
+
}
|
|
195
|
+
/** Decodes and validates FormData using the schema's input-side form model. */
|
|
196
|
+
async function parseFormData(schema, formData, options = {}) {
|
|
197
|
+
return parseFormDataWithModel(schema, compileForm(schema, options.config ?? {}), formData);
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/error.ts
|
|
201
|
+
function messages(value) {
|
|
202
|
+
if (value === void 0) return [];
|
|
203
|
+
return typeof value === "string" ? [value] : [...value];
|
|
204
|
+
}
|
|
205
|
+
function normalizedFieldErrors(errors) {
|
|
206
|
+
if (!errors) return {};
|
|
207
|
+
return Object.fromEntries(Object.entries(errors).map(([path, value]) => [path, messages(value)]));
|
|
208
|
+
}
|
|
209
|
+
/** A deliberate, user-safe business failure raised by a submission handler. */
|
|
210
|
+
var FormAdapterServerError = class extends Error {
|
|
211
|
+
name = "FormAdapterServerError";
|
|
212
|
+
fieldErrors;
|
|
213
|
+
formErrors;
|
|
214
|
+
constructor(options = {}) {
|
|
215
|
+
const fieldErrors = normalizedFieldErrors(options.fieldErrors);
|
|
216
|
+
const formErrors = messages(options.formErrors);
|
|
217
|
+
const firstFieldMessage = Object.values(fieldErrors)[0]?.[0];
|
|
218
|
+
const message = options.message ?? formErrors[0] ?? firstFieldMessage ?? "Submission failed";
|
|
219
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
220
|
+
this.fieldErrors = fieldErrors;
|
|
221
|
+
this.formErrors = formErrors;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
/** Creates a business error intended to be thrown from a submission handler. */
|
|
225
|
+
function formError(errorMessages, options = {}) {
|
|
226
|
+
return new FormAdapterServerError({
|
|
227
|
+
...options,
|
|
228
|
+
formErrors: errorMessages
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/** Creates a field-scoped business error intended to be thrown from a handler. */
|
|
232
|
+
function fieldError(path, errorMessages, options = {}) {
|
|
233
|
+
if (!path) throw new TypeError("A field error requires a non-empty path");
|
|
234
|
+
return new FormAdapterServerError({
|
|
235
|
+
...options,
|
|
236
|
+
fieldErrors: { [path]: errorMessages }
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/submission.ts
|
|
241
|
+
function isFormData(value) {
|
|
242
|
+
return typeof FormData !== "undefined" && value instanceof FormData;
|
|
243
|
+
}
|
|
244
|
+
function handlerContext(payload, context) {
|
|
245
|
+
const formData = isFormData(payload) ? payload : void 0;
|
|
246
|
+
const forwarded = { ...context };
|
|
247
|
+
delete forwarded.formData;
|
|
248
|
+
delete forwarded.payloadKind;
|
|
249
|
+
return {
|
|
250
|
+
...forwarded,
|
|
251
|
+
...formData ? { formData } : {},
|
|
252
|
+
payloadKind: formData ? "form-data" : "json"
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function createSubmissionHandler(schema, handler, options = {}) {
|
|
256
|
+
const model = compileForm(schema, options.config ?? {});
|
|
257
|
+
return async (payload, context) => {
|
|
258
|
+
const parsed = isFormData(payload) ? await parseFormDataWithModel(schema, model, payload) : await validateSubmissionValue(schema, model, payload);
|
|
259
|
+
if (!parsed.success) return submissionFailure({
|
|
260
|
+
errorKind: "validation",
|
|
261
|
+
fieldErrors: parsed.fieldErrors,
|
|
262
|
+
formErrors: parsed.formErrors
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
const result = await handler(parsed.data, handlerContext(payload, context));
|
|
266
|
+
if (isSubmissionState(result)) return result;
|
|
267
|
+
return submissionSuccess(result);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error instanceof FormAdapterServerError) return submissionFailure({
|
|
270
|
+
errorKind: "business",
|
|
271
|
+
fieldErrors: error.fieldErrors,
|
|
272
|
+
formErrors: error.formErrors
|
|
273
|
+
});
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/** Binds a transport invocation-context type while preserving schema/data inference. */
|
|
279
|
+
function createSubmissionHandlerFactory() {
|
|
280
|
+
return createSubmissionHandler;
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/transports.ts
|
|
284
|
+
function jsonResponse(state, status, headers) {
|
|
285
|
+
const responseHeaders = new Headers(headers);
|
|
286
|
+
responseHeaders.set("content-type", "application/json; charset=utf-8");
|
|
287
|
+
return new Response(JSON.stringify(state), {
|
|
288
|
+
headers: responseHeaders,
|
|
289
|
+
status
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function transportFailure(message) {
|
|
293
|
+
return submissionFailure({
|
|
294
|
+
errorKind: "transport",
|
|
295
|
+
formErrors: [message]
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function responseStatus(state) {
|
|
299
|
+
if (state.status !== "error") return 200;
|
|
300
|
+
return state.errorKind === "transport" ? 400 : 422;
|
|
301
|
+
}
|
|
302
|
+
function serverActionState(state) {
|
|
303
|
+
if (state.status !== "error") return state;
|
|
304
|
+
return {
|
|
305
|
+
...state,
|
|
306
|
+
fieldErrors: Object.fromEntries(Object.entries(state.fieldErrors).map(([path, messages]) => [path, [...messages]])),
|
|
307
|
+
formErrors: [...state.formErrors]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function mediaType(request) {
|
|
311
|
+
return request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
312
|
+
}
|
|
313
|
+
function isJsonMediaType(type) {
|
|
314
|
+
return type === "application/json" || type.endsWith("+json");
|
|
315
|
+
}
|
|
316
|
+
function isFormMediaType(type) {
|
|
317
|
+
return type === "multipart/form-data" || type === "application/x-www-form-urlencoded";
|
|
318
|
+
}
|
|
319
|
+
/** Adapts a reusable submission to React/Next-style `(state, FormData)` actions. */
|
|
320
|
+
function toServerAction(submission) {
|
|
321
|
+
return async (previousState, formData) => serverActionState(await submission(formData, { previousState }));
|
|
322
|
+
}
|
|
323
|
+
/** Creates a React/Next-style server action directly from a schema and handler. */
|
|
324
|
+
function createServerAction(schema, handler, options = {}) {
|
|
325
|
+
return toServerAction(createSubmissionHandler(schema, handler, options));
|
|
326
|
+
}
|
|
327
|
+
/** Adapts a reusable submission to a POST JSON/FormData HTTP request handler. */
|
|
328
|
+
function toRequestHandler(submission) {
|
|
329
|
+
return async (request) => {
|
|
330
|
+
if (request.method.toUpperCase() !== "POST") return jsonResponse(transportFailure("Only POST requests are supported."), 405, { allow: "POST" });
|
|
331
|
+
const type = mediaType(request);
|
|
332
|
+
if (!isJsonMediaType(type) && !isFormMediaType(type)) return jsonResponse(transportFailure("Expected a JSON or form request body."), 415);
|
|
333
|
+
let payload;
|
|
334
|
+
try {
|
|
335
|
+
payload = isJsonMediaType(type) ? await request.json() : await request.formData();
|
|
336
|
+
} catch {
|
|
337
|
+
return jsonResponse(transportFailure("Unable to read the request body."), 400);
|
|
338
|
+
}
|
|
339
|
+
const state = await submission(payload, { request });
|
|
340
|
+
return jsonResponse(state, responseStatus(state));
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/** Creates a JSON/FormData HTTP handler directly from a schema and handler. */
|
|
344
|
+
function createRequestHandler(schema, handler, options = {}) {
|
|
345
|
+
return toRequestHandler(createSubmissionHandler(schema, handler, options));
|
|
346
|
+
}
|
|
347
|
+
//#endregion
|
|
348
|
+
export { FORMADAPTER_ARRAY_MARKER, FORMADAPTER_BOOLEAN_MARKER, FORMADAPTER_VALUE_MARKER, FormAdapterServerError, createRequestHandler, createServerAction, createSubmissionHandler, createSubmissionHandlerFactory, fieldError, formError, parseFormData, toRequestHandler, toServerAction };
|
|
349
|
+
|
|
350
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/form-data.ts","../src/error.ts","../src/submission.ts","../src/transports.ts"],"sourcesContent":["import {\n compileForm,\n issuePath,\n isReservedFormPathSegment,\n optionForSerializedValue,\n pathToConfigPath,\n pathToName,\n prepareFormValues,\n validatePresentationRules,\n} from \"@formadapter/core\";\nimport type {\n DeepPartial,\n FormModel,\n FormNode,\n FormSchema,\n InferInput,\n InferOutput,\n ScalarField,\n StandardIssue,\n} from \"@formadapter/core\";\n\nimport type {\n ParseFormDataFailure,\n ParseFormDataResult,\n SubmissionOptions,\n} from \"./types\";\n\nexport const FORMADAPTER_ARRAY_MARKER = \"__formadapter_array\";\nexport const FORMADAPTER_BOOLEAN_MARKER = \"__formadapter_boolean\";\nexport const FORMADAPTER_VALUE_MARKER = \"__formadapter_value\";\n\nconst OMIT = Symbol(\"formadapter.omit\");\nconst MAX_ARRAY_INDEX = 10_000;\n\ntype ConcretePath = readonly (number | string)[];\ntype MutableRecord = Record<string, unknown>;\n\nfunction assertFormDataFieldNames<Input, Control extends string>(\n model: FormModel<Input, Control>,\n): void {\n const reserved: string[] = [];\n const visit = (node: FormNode<Control, Input>, root = false): void => {\n if (!root && isReservedFormPathSegment(node.key)) {\n reserved.push(node.path || node.key);\n }\n if (node.kind === \"object\") {\n for (const child of node.children) visit(child);\n } else if (node.kind === \"array\") {\n visit(node.item);\n }\n };\n visit(model.root, true);\n if (reserved.length === 0) return;\n const names = reserved.map((name) => JSON.stringify(name)).join(\", \");\n throw new TypeError(\n `Cannot decode FormData for unrepresentable field path ${names}. ` +\n \"Rename the field or use a JSON-only submission.\",\n );\n}\n\nfunction own(container: object, key: number | string): boolean {\n return Object.prototype.hasOwnProperty.call(container, key);\n}\n\nfunction concretePath(name: string): ConcretePath | undefined {\n if (!name || name.startsWith(\"$ACTION_\")) return undefined;\n const parts = name.split(\".\");\n const result: Array<number | string> = [];\n for (const part of parts) {\n if (/^(?:0|[1-9]\\d*)$/.test(part)) {\n const index = Number(part);\n if (!Number.isSafeInteger(index) || index > MAX_ARRAY_INDEX) {\n return undefined;\n }\n result.push(index);\n } else {\n if (isReservedFormPathSegment(part)) return undefined;\n result.push(part);\n }\n }\n return result;\n}\n\nfunction nodeAtPath<Input, Control extends string>(\n model: FormModel<Input, Control>,\n path: ConcretePath,\n): FormNode<Control, Input> | undefined {\n return model.fieldMap[pathToConfigPath(path)];\n}\n\nfunction createContainer(next: number | string): MutableRecord | unknown[] {\n return typeof next === \"number\" ? [] : {};\n}\n\nfunction valueAtPath(root: MutableRecord, path: ConcretePath): unknown {\n let current: unknown = root;\n for (const segment of path) {\n if (\n (typeof current !== \"object\" || current === null) ||\n !own(current, segment)\n ) {\n return undefined;\n }\n current = (current as MutableRecord)[segment];\n }\n return current;\n}\n\nfunction hasPath(root: MutableRecord, path: ConcretePath): boolean {\n let current: unknown = root;\n for (const segment of path) {\n if (\n (typeof current !== \"object\" || current === null) ||\n !own(current, segment)\n ) {\n return false;\n }\n current = (current as MutableRecord)[segment];\n }\n return true;\n}\n\nfunction setPath(\n root: MutableRecord,\n path: ConcretePath,\n value: unknown,\n): void {\n let current: MutableRecord | unknown[] = root;\n for (const [position, segment] of path.entries()) {\n const last = position === path.length - 1;\n if (last) {\n (current as MutableRecord)[segment] = value;\n return;\n }\n\n const next = path[position + 1]!;\n const existing = (current as MutableRecord)[segment];\n const needsArray = typeof next === \"number\";\n const usable =\n typeof existing === \"object\" &&\n existing !== null &&\n (needsArray ? Array.isArray(existing) : !Array.isArray(existing));\n if (!usable) {\n (current as MutableRecord)[segment] = createContainer(next);\n }\n current = (current as MutableRecord)[segment] as MutableRecord | unknown[];\n }\n}\n\nfunction appendPath(\n root: MutableRecord,\n path: ConcretePath,\n value: unknown,\n): void {\n const existing = valueAtPath(root, path);\n if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n setPath(root, path, [value]);\n }\n}\n\nfunction isFile(value: FormDataEntryValue): value is File {\n return typeof value !== \"string\";\n}\n\nfunction emptyFile(value: File): boolean {\n return value.name === \"\" && value.size === 0;\n}\n\nfunction serializedOption(value: string): unknown | typeof OMIT {\n const separator = value.indexOf(\":\");\n if (separator === -1) return OMIT;\n const type = value.slice(0, separator);\n const serialized = value.slice(separator + 1);\n switch (type) {\n case \"boolean\":\n if (serialized === \"true\") return true;\n if (serialized === \"false\") return false;\n return OMIT;\n case \"null\":\n return serialized === \"\" ? null : OMIT;\n case \"number\": {\n const number = Number(serialized);\n return serialized.trim() && Number.isFinite(number) ? number : OMIT;\n }\n case \"string\":\n return serialized;\n default:\n return OMIT;\n }\n}\n\nfunction hasOptions<Input, Control extends string>(\n node: ScalarField<Control, Input>,\n): boolean {\n return Boolean(\n node.options ||\n node.config.options ||\n node.control === \"radio\" ||\n node.control === \"select\",\n );\n}\n\nfunction decodeOption<Input, Control extends string>(\n node: ScalarField<Control, Input>,\n value: string,\n): unknown | typeof OMIT {\n const staticOption = node.options\n ? optionForSerializedValue(node.options, value)\n : undefined;\n if (staticOption) return staticOption.value;\n if (!hasOptions(node)) return OMIT;\n return serializedOption(value);\n}\n\nfunction decodeBoolean(value: string): boolean | string {\n if (value === \"on\" || value === \"1\" || value === \"true\") return true;\n if (value === \"0\" || value === \"false\") return false;\n return value;\n}\n\nfunction decodeScalar<Input, Control extends string>(\n node: ScalarField<Control, Input>,\n entry: FormDataEntryValue,\n typedValue = false,\n): unknown | typeof OMIT {\n if (node.dataType === \"file\") {\n if (!isFile(entry)) return entry;\n if (!emptyFile(entry)) return entry;\n return node.nullable ? null : OMIT;\n }\n if (isFile(entry)) return entry;\n\n if (typedValue) {\n const typed = serializedOption(entry);\n if (typed !== OMIT) return typed;\n }\n\n const option = decodeOption(node, entry);\n if (option !== OMIT) return option;\n if (entry === \"\" && node.nullable) return null;\n\n if (node.dataType === \"number\" || node.dataType === \"integer\") {\n if (!entry.trim()) return OMIT;\n const number = Number(entry);\n return Number.isFinite(number) ? number : entry;\n }\n if (node.dataType === \"boolean\") return decodeBoolean(entry);\n return entry;\n}\n\nfunction markerPaths(formData: FormData, marker: string): ConcretePath[] {\n return formData\n .getAll(marker)\n .filter((value): value is string => typeof value === \"string\")\n .map(concretePath)\n .filter((path): path is ConcretePath => path !== undefined);\n}\n\n/** Model-driven decoding. This is intentionally not exported from the package. */\nexport function decodeFormData<Input, Control extends string>(\n model: FormModel<Input, Control>,\n formData: FormData,\n): DeepPartial<Input> {\n const raw: MutableRecord = {};\n const typedPaths = new Set(\n markerPaths(formData, FORMADAPTER_VALUE_MARKER).map(pathToName),\n );\n\n for (const path of markerPaths(formData, FORMADAPTER_ARRAY_MARKER)) {\n if (nodeAtPath(model, path)?.kind === \"array\" && !hasPath(raw, path)) {\n setPath(raw, path, []);\n }\n }\n for (const path of markerPaths(formData, FORMADAPTER_BOOLEAN_MARKER)) {\n const node = nodeAtPath(model, path);\n if (\n node?.kind === \"scalar\" &&\n node.dataType === \"boolean\" &&\n !hasPath(raw, path)\n ) {\n setPath(raw, path, false);\n }\n }\n\n for (const [name, entry] of formData.entries()) {\n if (\n name === FORMADAPTER_ARRAY_MARKER ||\n name === FORMADAPTER_BOOLEAN_MARKER ||\n name === FORMADAPTER_VALUE_MARKER ||\n name.startsWith(\"$ACTION_\")\n ) {\n continue;\n }\n const path = concretePath(name);\n if (!path) continue;\n const node = nodeAtPath(model, path);\n if (!node) continue;\n\n if (node.kind === \"scalar\") {\n const decoded = decodeScalar(node, entry, typedPaths.has(name));\n if (decoded !== OMIT) setPath(raw, path, decoded);\n continue;\n }\n\n if (node.kind === \"array\" && node.item.kind === \"scalar\") {\n const decoded = decodeScalar(node.item, entry, typedPaths.has(name));\n if (decoded !== OMIT) appendPath(raw, path, decoded);\n }\n }\n\n return raw as DeepPartial<Input>;\n}\n\nfunction issuesFailure(\n issues: readonly StandardIssue[],\n): ParseFormDataFailure {\n const fieldErrors: Record<string, string[]> = Object.create(null);\n const formErrors: string[] = [];\n for (const issue of issues) {\n const path = pathToName(\n issuePath(issue).map((segment) =>\n typeof segment === \"number\" || typeof segment === \"string\"\n ? segment\n : String(segment),\n ),\n );\n if (!path) {\n formErrors.push(issue.message);\n } else {\n (fieldErrors[path] ??= []).push(issue.message);\n }\n }\n return { success: false, fieldErrors, formErrors };\n}\n\nexport async function validateSubmissionValue<\n Schema extends FormSchema,\n Control extends string,\n>(\n schema: Schema,\n model: FormModel<InferInput<Schema>, Control>,\n value: unknown,\n): Promise<ParseFormDataResult<InferOutput<Schema>>> {\n const presentationIssues = validatePresentationRules(\n model,\n value as DeepPartial<InferInput<Schema>>,\n );\n const result = await schema[\"~standard\"].validate(value);\n if (result.issues !== undefined) {\n const schemaIssues = result.issues.length > 0\n ? result.issues\n : [{ message: \"Schema validation failed\" }];\n return issuesFailure([...schemaIssues, ...presentationIssues]);\n }\n return presentationIssues.length > 0\n ? issuesFailure(presentationIssues)\n : { success: true, data: result.value };\n}\n\nexport async function parseFormDataWithModel<\n Schema extends FormSchema,\n Control extends string,\n>(\n schema: Schema,\n model: FormModel<InferInput<Schema>, Control>,\n formData: FormData,\n): Promise<ParseFormDataResult<InferOutput<Schema>>> {\n assertFormDataFieldNames(model);\n const decoded = decodeFormData(model, formData);\n const prepared = prepareFormValues(model, decoded);\n return validateSubmissionValue(schema, model, prepared);\n}\n\n/** Decodes and validates FormData using the schema's input-side form model. */\nexport async function parseFormData<\n Schema extends FormSchema,\n>(\n schema: Schema,\n formData: FormData,\n options: SubmissionOptions<Schema> = {},\n): Promise<ParseFormDataResult<InferOutput<Schema>>> {\n const model = compileForm(schema, options.config ?? {});\n return parseFormDataWithModel(schema, model, formData);\n}\n","export type ErrorMessages = readonly string[] | string;\n\nexport interface FormAdapterServerErrorOptions {\n readonly cause?: unknown;\n readonly fieldErrors?: Readonly<Record<string, ErrorMessages>>;\n readonly formErrors?: ErrorMessages;\n readonly message?: string;\n}\n\nexport interface ErrorHelperOptions {\n readonly cause?: unknown;\n readonly message?: string;\n}\n\nfunction messages(value: ErrorMessages | undefined): readonly string[] {\n if (value === undefined) return [];\n return typeof value === \"string\" ? [value] : [...value];\n}\n\nfunction normalizedFieldErrors(\n errors: Readonly<Record<string, ErrorMessages>> | undefined,\n): Readonly<Record<string, readonly string[]>> {\n if (!errors) return {};\n return Object.fromEntries(\n Object.entries(errors).map(([path, value]) => [path, messages(value)]),\n );\n}\n\n/** A deliberate, user-safe business failure raised by a submission handler. */\nexport class FormAdapterServerError extends Error {\n public override readonly name = \"FormAdapterServerError\";\n public readonly fieldErrors: Readonly<Record<string, readonly string[]>>;\n public readonly formErrors: readonly string[];\n\n public constructor(options: FormAdapterServerErrorOptions = {}) {\n const fieldErrors = normalizedFieldErrors(options.fieldErrors);\n const formErrors = messages(options.formErrors);\n const firstFieldMessage = Object.values(fieldErrors)[0]?.[0];\n const message =\n options.message ??\n formErrors[0] ??\n firstFieldMessage ??\n \"Submission failed\";\n super(\n message,\n options.cause === undefined ? undefined : { cause: options.cause },\n );\n this.fieldErrors = fieldErrors;\n this.formErrors = formErrors;\n }\n}\n\n/** Creates a business error intended to be thrown from a submission handler. */\nexport function formError(\n errorMessages: ErrorMessages,\n options: ErrorHelperOptions = {},\n): FormAdapterServerError {\n return new FormAdapterServerError({\n ...options,\n formErrors: errorMessages,\n });\n}\n\n/** Creates a field-scoped business error intended to be thrown from a handler. */\nexport function fieldError(\n path: string,\n errorMessages: ErrorMessages,\n options: ErrorHelperOptions = {},\n): FormAdapterServerError {\n if (!path) throw new TypeError(\"A field error requires a non-empty path\");\n return new FormAdapterServerError({\n ...options,\n fieldErrors: { [path]: errorMessages },\n });\n}\n","import {\n compileForm,\n isSubmissionState,\n submissionFailure,\n submissionSuccess,\n} from \"@formadapter/core\";\nimport type {\n FormSchema,\n SubmissionState,\n} from \"@formadapter/core\";\n\nimport { FormAdapterServerError } from \"./error\";\nimport {\n parseFormDataWithModel,\n validateSubmissionValue,\n} from \"./form-data\";\nimport type {\n CreatedSubmissionHandler,\n ContextualCreatedSubmissionHandler,\n CreateSubmissionHandler,\n SubmissionHandlerContext,\n SubmissionInvocationContext,\n SubmissionOptions,\n SubmissionValueHandler,\n} from \"./types\";\n\nfunction isFormData(value: unknown): value is FormData {\n return typeof FormData !== \"undefined\" && value instanceof FormData;\n}\n\nfunction handlerContext<Data, InvocationContext extends object>(\n payload: unknown,\n context: InvocationContext | undefined,\n): SubmissionHandlerContext<Data, InvocationContext> {\n const formData = isFormData(payload) ? payload : undefined;\n const forwarded = { ...context } as Record<string, unknown>;\n delete forwarded.formData;\n delete forwarded.payloadKind;\n return {\n ...forwarded,\n ...(formData ? { formData } : {}),\n payloadKind: formData ? \"form-data\" : \"json\",\n } as SubmissionHandlerContext<Data, InvocationContext>;\n}\n\n/** Compiles a schema once and returns a transport-neutral submission function. */\nexport function createSubmissionHandler<\n Schema extends FormSchema,\n Data = unknown,\n>(\n schema: Schema,\n handler: SubmissionValueHandler<Schema, Data>,\n options?: SubmissionOptions<Schema>,\n): CreatedSubmissionHandler<Data>;\nexport function createSubmissionHandler<\n Schema extends FormSchema,\n Data,\n InvocationContext extends object,\n>(\n schema: Schema,\n handler: SubmissionValueHandler<Schema, Data, InvocationContext>,\n options?: SubmissionOptions<Schema>,\n): ContextualCreatedSubmissionHandler<Data, InvocationContext>;\nexport function createSubmissionHandler<\n Schema extends FormSchema,\n Data = unknown,\n InvocationContext extends object = SubmissionInvocationContext<Data>,\n>(\n schema: Schema,\n handler: SubmissionValueHandler<Schema, Data, InvocationContext>,\n options: SubmissionOptions<Schema> = {},\n): CreatedSubmissionHandler<Data, InvocationContext> {\n const model = compileForm(schema, options.config ?? {});\n\n return async (\n payload: unknown,\n context?: InvocationContext,\n ): Promise<SubmissionState<Data>> => {\n const parsed = isFormData(payload)\n ? await parseFormDataWithModel(schema, model, payload)\n : await validateSubmissionValue(schema, model, payload);\n\n if (!parsed.success) {\n return submissionFailure({\n errorKind: \"validation\",\n fieldErrors: parsed.fieldErrors,\n formErrors: parsed.formErrors,\n });\n }\n\n try {\n const result = await handler(parsed.data, handlerContext(payload, context));\n if (isSubmissionState(result)) {\n return result as SubmissionState<Data>;\n }\n return submissionSuccess<Data>(result as Data);\n } catch (error) {\n if (error instanceof FormAdapterServerError) {\n return submissionFailure({\n errorKind: \"business\",\n fieldErrors: error.fieldErrors,\n formErrors: error.formErrors,\n });\n }\n throw error;\n }\n };\n}\n\n/** Binds a transport invocation-context type while preserving schema/data inference. */\nexport function createSubmissionHandlerFactory<\n InvocationContext extends object,\n>(): CreateSubmissionHandler<InvocationContext> {\n return createSubmissionHandler as CreateSubmissionHandler<InvocationContext>;\n}\n","import { submissionFailure } from \"@formadapter/core\";\nimport type { FormSchema, SubmissionState } from \"@formadapter/core\";\n\nimport { createSubmissionHandler } from \"./submission\";\nimport type {\n CreatedSubmissionHandler,\n RequestHandler,\n ServerAction,\n SubmissionOptions,\n SubmissionValueHandler,\n} from \"./types\";\n\nfunction jsonResponse(\n state: SubmissionState,\n status: number,\n headers?: HeadersInit,\n): Response {\n const responseHeaders = new Headers(headers);\n responseHeaders.set(\"content-type\", \"application/json; charset=utf-8\");\n return new Response(JSON.stringify(state), {\n headers: responseHeaders,\n status,\n });\n}\n\nfunction transportFailure(message: string): ReturnType<typeof submissionFailure> {\n return submissionFailure({\n errorKind: \"transport\",\n formErrors: [message],\n });\n}\n\nfunction responseStatus(state: SubmissionState): number {\n if (state.status !== \"error\") return 200;\n return state.errorKind === \"transport\" ? 400 : 422;\n}\n\nfunction serverActionState<Data>(\n state: SubmissionState<Data>,\n): SubmissionState<Data> {\n if (state.status !== \"error\") return state;\n return {\n ...state,\n fieldErrors: Object.fromEntries(\n Object.entries(state.fieldErrors).map(([path, messages]) => [\n path,\n [...messages],\n ]),\n ),\n formErrors: [...state.formErrors],\n };\n}\n\nfunction mediaType(request: Request): string {\n return request.headers.get(\"content-type\")?.split(\";\", 1)[0]?.trim().toLowerCase() ?? \"\";\n}\n\nfunction isJsonMediaType(type: string): boolean {\n return type === \"application/json\" || type.endsWith(\"+json\");\n}\n\nfunction isFormMediaType(type: string): boolean {\n return type === \"multipart/form-data\" || type === \"application/x-www-form-urlencoded\";\n}\n\n/** Adapts a reusable submission to React/Next-style `(state, FormData)` actions. */\nexport function toServerAction<Data>(\n submission: CreatedSubmissionHandler<Data>,\n): ServerAction<Data> {\n return async (previousState, formData) =>\n serverActionState(await submission(formData, { previousState }));\n}\n\n/** Creates a React/Next-style server action directly from a schema and handler. */\nexport function createServerAction<\n Schema extends FormSchema,\n Data = unknown,\n>(\n schema: Schema,\n handler: SubmissionValueHandler<Schema, Data>,\n options: SubmissionOptions<Schema> = {},\n): ServerAction<Data> {\n return toServerAction(createSubmissionHandler(schema, handler, options));\n}\n\n/** Adapts a reusable submission to a POST JSON/FormData HTTP request handler. */\nexport function toRequestHandler<Data>(\n submission: CreatedSubmissionHandler<Data>,\n): RequestHandler {\n return async (request): Promise<Response> => {\n if (request.method.toUpperCase() !== \"POST\") {\n return jsonResponse(\n transportFailure(\"Only POST requests are supported.\"),\n 405,\n { allow: \"POST\" },\n );\n }\n\n const type = mediaType(request);\n if (!isJsonMediaType(type) && !isFormMediaType(type)) {\n return jsonResponse(\n transportFailure(\"Expected a JSON or form request body.\"),\n 415,\n );\n }\n\n let payload: unknown;\n try {\n payload = isJsonMediaType(type)\n ? await request.json()\n : await request.formData();\n } catch {\n return jsonResponse(transportFailure(\"Unable to read the request body.\"), 400);\n }\n\n const state = await submission(payload, { request });\n return jsonResponse(state, responseStatus(state));\n };\n}\n\n/** Creates a JSON/FormData HTTP handler directly from a schema and handler. */\nexport function createRequestHandler<\n Schema extends FormSchema,\n Data = unknown,\n>(\n schema: Schema,\n handler: SubmissionValueHandler<Schema, Data>,\n options: SubmissionOptions<Schema> = {},\n): RequestHandler {\n return toRequestHandler(createSubmissionHandler(schema, handler, options));\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@formadapter/server",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Framework-neutral parsing, validation, and submission transports for FormAdapter.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"forms",
|
|
7
|
+
"server-actions",
|
|
8
|
+
"schema",
|
|
9
|
+
"standard-schema",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://formadapter.com",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ludicroushq/formadapter/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/ludicroushq/formadapter.git",
|
|
19
|
+
"directory": "packages/server"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "ludicrous",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"module": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js",
|
|
40
|
+
"default": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@formadapter/core": "^0.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"arktype": "^2.2.3",
|
|
49
|
+
"zod": "^4.4.3"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsdown",
|
|
53
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo .turbo",
|
|
54
|
+
"dev": "tsdown --watch",
|
|
55
|
+
"test": "vitest run --config vitest.config.ts",
|
|
56
|
+
"test:coverage": "vitest run --config vitest.config.ts --coverage",
|
|
57
|
+
"test:typecheck": "tsc --project tsconfig.test.json --noEmit",
|
|
58
|
+
"typecheck": "tsc --project tsconfig.json --noEmit"
|
|
59
|
+
}
|
|
60
|
+
}
|