server-act 1.6.0 → 1.7.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/README.md +144 -35
- package/dist/index.cjs +141 -0
- package/dist/{index.d.ts → index.d.cts} +33 -10
- package/dist/index.d.mts +33 -10
- package/dist/index.mjs +55 -12
- package/dist/utils.cjs +107 -0
- package/dist/utils.d.cts +74 -0
- package/dist/utils.d.mts +74 -0
- package/dist/utils.mjs +106 -0
- package/package.json +30 -32
- package/dist/index.js +0 -121
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ import { z } from "zod";
|
|
|
71
71
|
export const sayHelloAction = serverAct
|
|
72
72
|
.middleware(() => {
|
|
73
73
|
const t = i18n();
|
|
74
|
-
const userId = "..."
|
|
74
|
+
const userId = "...";
|
|
75
75
|
return { t, userId };
|
|
76
76
|
})
|
|
77
77
|
.input((ctx) => {
|
|
@@ -85,63 +85,172 @@ export const sayHelloAction = serverAct
|
|
|
85
85
|
});
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
#### Chaining Middlewares
|
|
89
|
+
|
|
90
|
+
You can chain multiple middlewares by calling `.middleware(...)` repeatedly.
|
|
91
|
+
|
|
92
|
+
- Middlewares run in registration order.
|
|
93
|
+
- Each middleware receives the current `ctx` and can return additional context.
|
|
94
|
+
- Returned objects are shallow-merged into `ctx`.
|
|
95
|
+
- Later middleware values override earlier values for the same key.
|
|
96
|
+
- Errors thrown in middleware propagate and stop later middleware from running.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// action.ts
|
|
100
|
+
"use server";
|
|
101
|
+
|
|
102
|
+
import { serverAct } from "server-act";
|
|
103
|
+
|
|
104
|
+
export const createGreetingAction = serverAct
|
|
105
|
+
.middleware(() => ({
|
|
106
|
+
requestId: crypto.randomUUID(),
|
|
107
|
+
role: "user",
|
|
108
|
+
}))
|
|
109
|
+
.middleware(({ ctx }) => ({
|
|
110
|
+
role: "admin", // overrides previous role
|
|
111
|
+
actorLabel: `${ctx.role}-actor`,
|
|
112
|
+
}))
|
|
113
|
+
.middleware(({ ctx }) => ({
|
|
114
|
+
trace: `${ctx.requestId}:${ctx.actorLabel}`,
|
|
115
|
+
}))
|
|
116
|
+
.action(async ({ ctx }) => {
|
|
117
|
+
return `${ctx.role} -> ${ctx.trace}`;
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
88
121
|
### `useActionState` Support
|
|
89
122
|
|
|
90
123
|
> `useActionState` Documentation:
|
|
91
124
|
>
|
|
92
125
|
> - https://react.dev/reference/react/useActionState
|
|
93
126
|
|
|
94
|
-
We recommend using [zod-form-data](https://www.npmjs.com/package/zod-form-data) for input validation.
|
|
95
|
-
|
|
96
127
|
```ts
|
|
97
128
|
// action.ts;
|
|
98
129
|
"use server";
|
|
99
130
|
|
|
100
131
|
import { serverAct } from "server-act";
|
|
132
|
+
import { formDataToObject } from "server-act/utils";
|
|
101
133
|
import { z } from "zod";
|
|
102
|
-
|
|
134
|
+
|
|
135
|
+
function zodFormData<T extends z.ZodType>(schema: T) {
|
|
136
|
+
return z.preprocess<Record<string, unknown>, T, FormData>(
|
|
137
|
+
(v) => formDataToObject(v),
|
|
138
|
+
schema,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
103
141
|
|
|
104
142
|
export const sayHelloAction = serverAct
|
|
105
143
|
.input(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
z
|
|
109
|
-
.string(
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
144
|
+
zodFormData(
|
|
145
|
+
z.object({
|
|
146
|
+
name: z
|
|
147
|
+
.string()
|
|
148
|
+
.min(1, { error: `You haven't told me your name` })
|
|
149
|
+
.max(20, { error: "Any shorter name? You name is too long 😬" }),
|
|
150
|
+
}),
|
|
151
|
+
),
|
|
113
152
|
)
|
|
114
|
-
.stateAction(async ({
|
|
115
|
-
if (
|
|
116
|
-
return { formData,
|
|
153
|
+
.stateAction(async ({ rawInput, input, inputErrors, ctx }) => {
|
|
154
|
+
if (inputErrors) {
|
|
155
|
+
return { formData: rawInput, inputErrors: inputErrors.fieldErrors };
|
|
117
156
|
}
|
|
118
157
|
return { message: `Hello, ${input.name}!` };
|
|
119
158
|
});
|
|
120
159
|
```
|
|
121
160
|
|
|
122
|
-
|
|
123
|
-
// client-component.tsx
|
|
124
|
-
"use client";
|
|
161
|
+
## Utilities
|
|
125
162
|
|
|
126
|
-
|
|
127
|
-
import { sayHelloAction } from "./action";
|
|
163
|
+
### `formDataToObject`
|
|
128
164
|
|
|
129
|
-
|
|
130
|
-
const [state, dispatch] = useActionState(sayHelloAction, undefined);
|
|
165
|
+
The `formDataToObject` utility converts FormData to a structured JavaScript object, supporting nested objects, arrays, and complex form structures.
|
|
131
166
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
167
|
+
```ts
|
|
168
|
+
import { formDataToObject } from "server-act/utils";
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Basic Usage
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const formData = new FormData();
|
|
175
|
+
formData.append("name", "John");
|
|
176
|
+
|
|
177
|
+
const result = formDataToObject(formData);
|
|
178
|
+
// Result: { name: 'John' }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Nested Objects and Arrays
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const formData = new FormData();
|
|
185
|
+
formData.append("user.name", "John");
|
|
186
|
+
|
|
187
|
+
const result = formDataToObject(formData);
|
|
188
|
+
// Result: { user: { name: 'John' } }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### With Zod
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
"use server";
|
|
195
|
+
|
|
196
|
+
import { serverAct } from "server-act";
|
|
197
|
+
import { formDataToObject } from "server-act/utils";
|
|
198
|
+
import { z } from "zod";
|
|
199
|
+
|
|
200
|
+
function zodFormData<T extends z.ZodType>(schema: T) {
|
|
201
|
+
return z.preprocess<Record<string, unknown>, T, FormData>(
|
|
202
|
+
(v) => formDataToObject(v),
|
|
203
|
+
schema,
|
|
145
204
|
);
|
|
146
|
-
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const createUserAction = serverAct
|
|
208
|
+
.input(
|
|
209
|
+
zodFormData(
|
|
210
|
+
z.object({
|
|
211
|
+
name: z.string().min(1, "Name is required"),
|
|
212
|
+
}),
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
.stateAction(async ({ rawInput, input, inputErrors }) => {
|
|
216
|
+
if (inputErrors) {
|
|
217
|
+
return { formData: rawInput, errors: inputErrors.fieldErrors };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Process the validated input
|
|
221
|
+
console.log("User:", input.name);
|
|
222
|
+
|
|
223
|
+
return { success: true, userId: "123" };
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
#### With Valibot
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
"use server";
|
|
231
|
+
|
|
232
|
+
import { serverAct } from "server-act";
|
|
233
|
+
import { formDataToObject } from "server-act/utils";
|
|
234
|
+
import * as v from "valibot";
|
|
235
|
+
|
|
236
|
+
export const createPostAction = serverAct
|
|
237
|
+
.input(
|
|
238
|
+
v.pipe(
|
|
239
|
+
v.custom<FormData>((value) => value instanceof FormData),
|
|
240
|
+
v.transform(formDataToObject),
|
|
241
|
+
v.object({
|
|
242
|
+
title: v.pipe(v.string(), v.minLength(1, "Title is required")),
|
|
243
|
+
}),
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
.stateAction(async ({ rawInput, input, inputErrors }) => {
|
|
247
|
+
if (inputErrors) {
|
|
248
|
+
return { formData: rawInput, errors: inputErrors.fieldErrors };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Process the validated input
|
|
252
|
+
console.log("Post:", input.title);
|
|
253
|
+
|
|
254
|
+
return { success: true, postId: "456" };
|
|
255
|
+
});
|
|
147
256
|
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
let _standard_schema_utils = require("@standard-schema/utils");
|
|
2
|
+
|
|
3
|
+
//#region src/internal/middleware.ts
|
|
4
|
+
/**
|
|
5
|
+
* Executes an array of middleware functions with the given initial context.
|
|
6
|
+
*/
|
|
7
|
+
async function executeMiddlewares(middlewares, initialCtx) {
|
|
8
|
+
let ctx = initialCtx && typeof initialCtx === "object" ? { ...initialCtx } : {};
|
|
9
|
+
for (const middleware of middlewares) {
|
|
10
|
+
const result = await middleware({ ctx });
|
|
11
|
+
if (result && typeof result === "object") ctx = {
|
|
12
|
+
...ctx,
|
|
13
|
+
...result
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return ctx;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/internal/schema.ts
|
|
21
|
+
async function standardValidate(schema, input) {
|
|
22
|
+
let result = schema["~standard"].validate(input);
|
|
23
|
+
if (result instanceof Promise) result = await result;
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
function getInputErrors(issues) {
|
|
27
|
+
const messages = [];
|
|
28
|
+
const fieldErrors = {};
|
|
29
|
+
for (const issue of issues) {
|
|
30
|
+
const dotPath = (0, _standard_schema_utils.getDotPath)(issue);
|
|
31
|
+
if (dotPath) if (fieldErrors[dotPath]) fieldErrors[dotPath].push(issue.message);
|
|
32
|
+
else fieldErrors[dotPath] = [issue.message];
|
|
33
|
+
else messages.push(issue.message);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
messages,
|
|
37
|
+
fieldErrors
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/index.ts
|
|
43
|
+
function createNewServerActionBuilder(def) {
|
|
44
|
+
return createServerActionBuilder(def);
|
|
45
|
+
}
|
|
46
|
+
function createServerActionBuilder(initDef = {}) {
|
|
47
|
+
const _def = {
|
|
48
|
+
input: void 0,
|
|
49
|
+
middleware: [],
|
|
50
|
+
...initDef
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
middleware: (middleware) => createNewServerActionBuilder({
|
|
54
|
+
..._def,
|
|
55
|
+
middleware: [..._def.middleware, middleware]
|
|
56
|
+
}),
|
|
57
|
+
input: (input) => createNewServerActionBuilder({
|
|
58
|
+
..._def,
|
|
59
|
+
input
|
|
60
|
+
}),
|
|
61
|
+
action: (action) => {
|
|
62
|
+
return async (input) => {
|
|
63
|
+
let ctx = {};
|
|
64
|
+
if (_def.middleware.length > 0) ctx = await executeMiddlewares(_def.middleware, ctx);
|
|
65
|
+
if (_def.input) {
|
|
66
|
+
const result = await standardValidate(typeof _def.input === "function" ? await _def.input({ ctx }) : _def.input, input);
|
|
67
|
+
if (result.issues) throw new _standard_schema_utils.SchemaError(result.issues);
|
|
68
|
+
return await action({
|
|
69
|
+
ctx,
|
|
70
|
+
input: result.value
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return await action({
|
|
74
|
+
ctx,
|
|
75
|
+
input: void 0
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
stateAction: (action) => {
|
|
80
|
+
return async (prevState, rawInput) => {
|
|
81
|
+
let ctx = {};
|
|
82
|
+
if (_def.middleware.length > 0) ctx = await executeMiddlewares(_def.middleware, ctx);
|
|
83
|
+
if (_def.input) {
|
|
84
|
+
const result = await standardValidate(typeof _def.input === "function" ? await _def.input({ ctx }) : _def.input, rawInput);
|
|
85
|
+
if (result.issues) return await action({
|
|
86
|
+
ctx,
|
|
87
|
+
prevState,
|
|
88
|
+
rawInput,
|
|
89
|
+
inputErrors: getInputErrors(result.issues)
|
|
90
|
+
});
|
|
91
|
+
return await action({
|
|
92
|
+
ctx,
|
|
93
|
+
prevState,
|
|
94
|
+
rawInput,
|
|
95
|
+
input: result.value
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return await action({
|
|
99
|
+
ctx,
|
|
100
|
+
prevState,
|
|
101
|
+
rawInput,
|
|
102
|
+
input: void 0
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
formAction: (action) => {
|
|
107
|
+
return async (prevState, formData) => {
|
|
108
|
+
let ctx = {};
|
|
109
|
+
if (_def.middleware.length > 0) ctx = await executeMiddlewares(_def.middleware, ctx);
|
|
110
|
+
if (_def.input) {
|
|
111
|
+
const result = await standardValidate(typeof _def.input === "function" ? await _def.input({ ctx }) : _def.input, formData);
|
|
112
|
+
if (result.issues) return await action({
|
|
113
|
+
ctx,
|
|
114
|
+
prevState,
|
|
115
|
+
formData,
|
|
116
|
+
formErrors: getInputErrors(result.issues)
|
|
117
|
+
});
|
|
118
|
+
return await action({
|
|
119
|
+
ctx,
|
|
120
|
+
prevState,
|
|
121
|
+
formData,
|
|
122
|
+
input: result.value
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return await action({
|
|
126
|
+
ctx,
|
|
127
|
+
prevState,
|
|
128
|
+
formData,
|
|
129
|
+
input: void 0
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Server action builder
|
|
137
|
+
*/
|
|
138
|
+
const serverAct = createServerActionBuilder();
|
|
139
|
+
|
|
140
|
+
//#endregion
|
|
141
|
+
exports.serverAct = serverAct;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
4
|
-
|
|
3
|
+
//#region src/internal/middleware.d.ts
|
|
4
|
+
type MiddlewareFunction<TContext, TReturn> = (params: {
|
|
5
|
+
ctx: TContext;
|
|
6
|
+
}) => Promise<TReturn> | TReturn;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/internal/schema.d.ts
|
|
9
|
+
declare function getInputErrors(issues: ReadonlyArray<StandardSchemaV1.Issue>): {
|
|
5
10
|
messages: string[];
|
|
6
11
|
fieldErrors: Record<string, string[]>;
|
|
7
12
|
};
|
|
@@ -15,7 +20,6 @@ type Prettify<T> = { [P in keyof T]: T[P] } & {};
|
|
|
15
20
|
type SanitizeFunctionParam<T extends (param: any) => any> = T extends ((param: infer P) => infer R) ? Equals<P, undefined> extends true ? () => R : Equals<P, P | undefined> extends true ? (param?: P) => R : (param: P) => R : never;
|
|
16
21
|
type InferParserType<T, TType extends "in" | "out"> = T extends StandardSchemaV1 ? TType extends "in" ? StandardSchemaV1.InferInput<T> : StandardSchemaV1.InferOutput<T> : never;
|
|
17
22
|
type InferInputType<T, TType extends "in" | "out"> = T extends UnsetMarker ? undefined : InferParserType<T, TType>;
|
|
18
|
-
type InferContextType<T> = RemoveUnsetMarker<T>;
|
|
19
23
|
interface ActionParams<TInput = unknown, TContext = unknown> {
|
|
20
24
|
_input: TInput;
|
|
21
25
|
_context: TContext;
|
|
@@ -23,16 +27,19 @@ interface ActionParams<TInput = unknown, TContext = unknown> {
|
|
|
23
27
|
interface ActionBuilder<TParams extends ActionParams> {
|
|
24
28
|
/**
|
|
25
29
|
* Middleware allows you to run code before the action, its return value will pass as context to the action.
|
|
30
|
+
*
|
|
31
|
+
* Chaining multiple middlewares is possible, each middleware receives context from previous middlewares
|
|
32
|
+
* and returns additional context that gets merged.
|
|
26
33
|
*/
|
|
27
|
-
middleware: <
|
|
34
|
+
middleware: <TNewContext>(middleware: MiddlewareFunction<RemoveUnsetMarker<TParams["_context"]>, TNewContext>) => ActionBuilder<{
|
|
28
35
|
_input: TParams["_input"];
|
|
29
|
-
_context:
|
|
30
|
-
}
|
|
36
|
+
_context: TParams["_context"] extends UnsetMarker ? TNewContext : Prettify<TParams["_context"] & TNewContext>;
|
|
37
|
+
}>;
|
|
31
38
|
/**
|
|
32
39
|
* Input validation for the action.
|
|
33
40
|
*/
|
|
34
41
|
input: <TParser extends StandardSchemaV1>(input: ((params: {
|
|
35
|
-
ctx:
|
|
42
|
+
ctx: RemoveUnsetMarker<TParams["_context"]>;
|
|
36
43
|
}) => Promise<TParser> | TParser) | TParser) => Omit<ActionBuilder<{
|
|
37
44
|
_input: TParser;
|
|
38
45
|
_context: TParams["_context"];
|
|
@@ -41,14 +48,30 @@ interface ActionBuilder<TParams extends ActionParams> {
|
|
|
41
48
|
* Create an action.
|
|
42
49
|
*/
|
|
43
50
|
action: <TOutput>(action: (params: {
|
|
44
|
-
ctx:
|
|
51
|
+
ctx: RemoveUnsetMarker<TParams["_context"]>;
|
|
45
52
|
input: InferInputType<TParams["_input"], "out">;
|
|
46
53
|
}) => Promise<TOutput>) => SanitizeFunctionParam<(input: InferInputType<TParams["_input"], "in">) => Promise<TOutput>>;
|
|
47
54
|
/**
|
|
48
55
|
* Create an action for React `useActionState`
|
|
49
56
|
*/
|
|
50
57
|
stateAction: <TState, TPrevState = UnsetMarker>(action: (params: Prettify<{
|
|
51
|
-
ctx:
|
|
58
|
+
ctx: RemoveUnsetMarker<TParams["_context"]>;
|
|
59
|
+
prevState: RemoveUnsetMarker<TPrevState>;
|
|
60
|
+
rawInput: InferInputType<TParams["_input"], "in">;
|
|
61
|
+
} & ({
|
|
62
|
+
input: InferInputType<TParams["_input"], "out">;
|
|
63
|
+
inputErrors?: undefined;
|
|
64
|
+
} | {
|
|
65
|
+
input?: undefined;
|
|
66
|
+
inputErrors: ReturnType<typeof getInputErrors>;
|
|
67
|
+
})>) => Promise<TState>) => (prevState: TState | RemoveUnsetMarker<TPrevState>, input: InferInputType<TParams["_input"], "in">) => Promise<TState | RemoveUnsetMarker<TPrevState>>;
|
|
68
|
+
/**
|
|
69
|
+
* Create an action for React `useActionState`
|
|
70
|
+
*
|
|
71
|
+
* @deprecated Use `stateAction` instead.
|
|
72
|
+
*/
|
|
73
|
+
formAction: <TState, TPrevState = UnsetMarker>(action: (params: Prettify<{
|
|
74
|
+
ctx: RemoveUnsetMarker<TParams["_context"]>;
|
|
52
75
|
prevState: RemoveUnsetMarker<TPrevState>;
|
|
53
76
|
formData: FormData;
|
|
54
77
|
} & ({
|
|
@@ -56,7 +79,7 @@ interface ActionBuilder<TParams extends ActionParams> {
|
|
|
56
79
|
formErrors?: undefined;
|
|
57
80
|
} | {
|
|
58
81
|
input?: undefined;
|
|
59
|
-
formErrors: ReturnType<typeof
|
|
82
|
+
formErrors: ReturnType<typeof getInputErrors>;
|
|
60
83
|
})>) => Promise<TState>) => (prevState: TState | RemoveUnsetMarker<TPrevState>, formData: InferInputType<TParams["_input"], "in">) => Promise<TState | RemoveUnsetMarker<TPrevState>>;
|
|
61
84
|
}
|
|
62
85
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
4
|
-
|
|
3
|
+
//#region src/internal/middleware.d.ts
|
|
4
|
+
type MiddlewareFunction<TContext, TReturn> = (params: {
|
|
5
|
+
ctx: TContext;
|
|
6
|
+
}) => Promise<TReturn> | TReturn;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/internal/schema.d.ts
|
|
9
|
+
declare function getInputErrors(issues: ReadonlyArray<StandardSchemaV1.Issue>): {
|
|
5
10
|
messages: string[];
|
|
6
11
|
fieldErrors: Record<string, string[]>;
|
|
7
12
|
};
|
|
@@ -15,7 +20,6 @@ type Prettify<T> = { [P in keyof T]: T[P] } & {};
|
|
|
15
20
|
type SanitizeFunctionParam<T extends (param: any) => any> = T extends ((param: infer P) => infer R) ? Equals<P, undefined> extends true ? () => R : Equals<P, P | undefined> extends true ? (param?: P) => R : (param: P) => R : never;
|
|
16
21
|
type InferParserType<T, TType extends "in" | "out"> = T extends StandardSchemaV1 ? TType extends "in" ? StandardSchemaV1.InferInput<T> : StandardSchemaV1.InferOutput<T> : never;
|
|
17
22
|
type InferInputType<T, TType extends "in" | "out"> = T extends UnsetMarker ? undefined : InferParserType<T, TType>;
|
|
18
|
-
type InferContextType<T> = RemoveUnsetMarker<T>;
|
|
19
23
|
interface ActionParams<TInput = unknown, TContext = unknown> {
|
|
20
24
|
_input: TInput;
|
|
21
25
|
_context: TContext;
|
|
@@ -23,16 +27,19 @@ interface ActionParams<TInput = unknown, TContext = unknown> {
|
|
|
23
27
|
interface ActionBuilder<TParams extends ActionParams> {
|
|
24
28
|
/**
|
|
25
29
|
* Middleware allows you to run code before the action, its return value will pass as context to the action.
|
|
30
|
+
*
|
|
31
|
+
* Chaining multiple middlewares is possible, each middleware receives context from previous middlewares
|
|
32
|
+
* and returns additional context that gets merged.
|
|
26
33
|
*/
|
|
27
|
-
middleware: <
|
|
34
|
+
middleware: <TNewContext>(middleware: MiddlewareFunction<RemoveUnsetMarker<TParams["_context"]>, TNewContext>) => ActionBuilder<{
|
|
28
35
|
_input: TParams["_input"];
|
|
29
|
-
_context:
|
|
30
|
-
}
|
|
36
|
+
_context: TParams["_context"] extends UnsetMarker ? TNewContext : Prettify<TParams["_context"] & TNewContext>;
|
|
37
|
+
}>;
|
|
31
38
|
/**
|
|
32
39
|
* Input validation for the action.
|
|
33
40
|
*/
|
|
34
41
|
input: <TParser extends StandardSchemaV1>(input: ((params: {
|
|
35
|
-
ctx:
|
|
42
|
+
ctx: RemoveUnsetMarker<TParams["_context"]>;
|
|
36
43
|
}) => Promise<TParser> | TParser) | TParser) => Omit<ActionBuilder<{
|
|
37
44
|
_input: TParser;
|
|
38
45
|
_context: TParams["_context"];
|
|
@@ -41,14 +48,30 @@ interface ActionBuilder<TParams extends ActionParams> {
|
|
|
41
48
|
* Create an action.
|
|
42
49
|
*/
|
|
43
50
|
action: <TOutput>(action: (params: {
|
|
44
|
-
ctx:
|
|
51
|
+
ctx: RemoveUnsetMarker<TParams["_context"]>;
|
|
45
52
|
input: InferInputType<TParams["_input"], "out">;
|
|
46
53
|
}) => Promise<TOutput>) => SanitizeFunctionParam<(input: InferInputType<TParams["_input"], "in">) => Promise<TOutput>>;
|
|
47
54
|
/**
|
|
48
55
|
* Create an action for React `useActionState`
|
|
49
56
|
*/
|
|
50
57
|
stateAction: <TState, TPrevState = UnsetMarker>(action: (params: Prettify<{
|
|
51
|
-
ctx:
|
|
58
|
+
ctx: RemoveUnsetMarker<TParams["_context"]>;
|
|
59
|
+
prevState: RemoveUnsetMarker<TPrevState>;
|
|
60
|
+
rawInput: InferInputType<TParams["_input"], "in">;
|
|
61
|
+
} & ({
|
|
62
|
+
input: InferInputType<TParams["_input"], "out">;
|
|
63
|
+
inputErrors?: undefined;
|
|
64
|
+
} | {
|
|
65
|
+
input?: undefined;
|
|
66
|
+
inputErrors: ReturnType<typeof getInputErrors>;
|
|
67
|
+
})>) => Promise<TState>) => (prevState: TState | RemoveUnsetMarker<TPrevState>, input: InferInputType<TParams["_input"], "in">) => Promise<TState | RemoveUnsetMarker<TPrevState>>;
|
|
68
|
+
/**
|
|
69
|
+
* Create an action for React `useActionState`
|
|
70
|
+
*
|
|
71
|
+
* @deprecated Use `stateAction` instead.
|
|
72
|
+
*/
|
|
73
|
+
formAction: <TState, TPrevState = UnsetMarker>(action: (params: Prettify<{
|
|
74
|
+
ctx: RemoveUnsetMarker<TParams["_context"]>;
|
|
52
75
|
prevState: RemoveUnsetMarker<TPrevState>;
|
|
53
76
|
formData: FormData;
|
|
54
77
|
} & ({
|
|
@@ -56,7 +79,7 @@ interface ActionBuilder<TParams extends ActionParams> {
|
|
|
56
79
|
formErrors?: undefined;
|
|
57
80
|
} | {
|
|
58
81
|
input?: undefined;
|
|
59
|
-
formErrors: ReturnType<typeof
|
|
82
|
+
formErrors: ReturnType<typeof getInputErrors>;
|
|
60
83
|
})>) => Promise<TState>) => (prevState: TState | RemoveUnsetMarker<TPrevState>, formData: InferInputType<TParams["_input"], "in">) => Promise<TState | RemoveUnsetMarker<TPrevState>>;
|
|
61
84
|
}
|
|
62
85
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { SchemaError, getDotPath } from "@standard-schema/utils";
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/internal/middleware.ts
|
|
4
|
+
/**
|
|
5
|
+
* Executes an array of middleware functions with the given initial context.
|
|
6
|
+
*/
|
|
7
|
+
async function executeMiddlewares(middlewares, initialCtx) {
|
|
8
|
+
let ctx = initialCtx && typeof initialCtx === "object" ? { ...initialCtx } : {};
|
|
9
|
+
for (const middleware of middlewares) {
|
|
10
|
+
const result = await middleware({ ctx });
|
|
11
|
+
if (result && typeof result === "object") ctx = {
|
|
12
|
+
...ctx,
|
|
13
|
+
...result
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return ctx;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/internal/schema.ts
|
|
4
21
|
async function standardValidate(schema, input) {
|
|
5
22
|
let result = schema["~standard"].validate(input);
|
|
6
23
|
if (result instanceof Promise) result = await result;
|
|
7
24
|
return result;
|
|
8
25
|
}
|
|
9
|
-
function
|
|
26
|
+
function getInputErrors(issues) {
|
|
10
27
|
const messages = [];
|
|
11
28
|
const fieldErrors = {};
|
|
12
29
|
for (const issue of issues) {
|
|
@@ -23,20 +40,19 @@ function getFormErrors(issues) {
|
|
|
23
40
|
|
|
24
41
|
//#endregion
|
|
25
42
|
//#region src/index.ts
|
|
26
|
-
const unsetMarker = Symbol("unsetMarker");
|
|
27
43
|
function createNewServerActionBuilder(def) {
|
|
28
44
|
return createServerActionBuilder(def);
|
|
29
45
|
}
|
|
30
46
|
function createServerActionBuilder(initDef = {}) {
|
|
31
47
|
const _def = {
|
|
32
48
|
input: void 0,
|
|
33
|
-
middleware:
|
|
49
|
+
middleware: [],
|
|
34
50
|
...initDef
|
|
35
51
|
};
|
|
36
52
|
return {
|
|
37
53
|
middleware: (middleware) => createNewServerActionBuilder({
|
|
38
54
|
..._def,
|
|
39
|
-
middleware
|
|
55
|
+
middleware: [..._def.middleware, middleware]
|
|
40
56
|
}),
|
|
41
57
|
input: (input) => createNewServerActionBuilder({
|
|
42
58
|
..._def,
|
|
@@ -44,10 +60,10 @@ function createServerActionBuilder(initDef = {}) {
|
|
|
44
60
|
}),
|
|
45
61
|
action: (action) => {
|
|
46
62
|
return async (input) => {
|
|
47
|
-
|
|
63
|
+
let ctx = {};
|
|
64
|
+
if (_def.middleware.length > 0) ctx = await executeMiddlewares(_def.middleware, ctx);
|
|
48
65
|
if (_def.input) {
|
|
49
|
-
const
|
|
50
|
-
const result = await standardValidate(inputSchema, input);
|
|
66
|
+
const result = await standardValidate(typeof _def.input === "function" ? await _def.input({ ctx }) : _def.input, input);
|
|
51
67
|
if (result.issues) throw new SchemaError(result.issues);
|
|
52
68
|
return await action({
|
|
53
69
|
ctx,
|
|
@@ -61,16 +77,43 @@ function createServerActionBuilder(initDef = {}) {
|
|
|
61
77
|
};
|
|
62
78
|
},
|
|
63
79
|
stateAction: (action) => {
|
|
80
|
+
return async (prevState, rawInput) => {
|
|
81
|
+
let ctx = {};
|
|
82
|
+
if (_def.middleware.length > 0) ctx = await executeMiddlewares(_def.middleware, ctx);
|
|
83
|
+
if (_def.input) {
|
|
84
|
+
const result = await standardValidate(typeof _def.input === "function" ? await _def.input({ ctx }) : _def.input, rawInput);
|
|
85
|
+
if (result.issues) return await action({
|
|
86
|
+
ctx,
|
|
87
|
+
prevState,
|
|
88
|
+
rawInput,
|
|
89
|
+
inputErrors: getInputErrors(result.issues)
|
|
90
|
+
});
|
|
91
|
+
return await action({
|
|
92
|
+
ctx,
|
|
93
|
+
prevState,
|
|
94
|
+
rawInput,
|
|
95
|
+
input: result.value
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return await action({
|
|
99
|
+
ctx,
|
|
100
|
+
prevState,
|
|
101
|
+
rawInput,
|
|
102
|
+
input: void 0
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
formAction: (action) => {
|
|
64
107
|
return async (prevState, formData) => {
|
|
65
|
-
|
|
108
|
+
let ctx = {};
|
|
109
|
+
if (_def.middleware.length > 0) ctx = await executeMiddlewares(_def.middleware, ctx);
|
|
66
110
|
if (_def.input) {
|
|
67
|
-
const
|
|
68
|
-
const result = await standardValidate(inputSchema, formData);
|
|
111
|
+
const result = await standardValidate(typeof _def.input === "function" ? await _def.input({ ctx }) : _def.input, formData);
|
|
69
112
|
if (result.issues) return await action({
|
|
70
113
|
ctx,
|
|
71
114
|
prevState,
|
|
72
115
|
formData,
|
|
73
|
-
formErrors:
|
|
116
|
+
formErrors: getInputErrors(result.issues)
|
|
74
117
|
});
|
|
75
118
|
return await action({
|
|
76
119
|
ctx,
|
package/dist/utils.cjs
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/internal/assert.ts
|
|
3
|
+
function assert(condition, message) {
|
|
4
|
+
if (!condition) throw new Error(message || "Assertion failed");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/utils.ts
|
|
9
|
+
function isNumberString(str) {
|
|
10
|
+
return /^\d+$/.test(str);
|
|
11
|
+
}
|
|
12
|
+
function set(obj, path, value) {
|
|
13
|
+
if (path.length > 1) {
|
|
14
|
+
const newPath = [...path];
|
|
15
|
+
const key = newPath.shift();
|
|
16
|
+
assert(key != null);
|
|
17
|
+
const nextKey = newPath[0];
|
|
18
|
+
assert(nextKey != null);
|
|
19
|
+
if (!obj[key]) obj[key] = isNumberString(nextKey) ? [] : {};
|
|
20
|
+
else if (Array.isArray(obj[key]) && !isNumberString(nextKey)) obj[key] = Object.fromEntries(Object.entries(obj[key]));
|
|
21
|
+
set(obj[key], newPath, value);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const p = path[0];
|
|
25
|
+
assert(p != null);
|
|
26
|
+
if (obj[p] === void 0) obj[p] = value;
|
|
27
|
+
else if (Array.isArray(obj[p])) obj[p].push(value);
|
|
28
|
+
else obj[p] = [obj[p], value];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Converts FormData to a structured JavaScript object
|
|
32
|
+
*
|
|
33
|
+
* This function parses FormData entries and converts them into a nested object structure.
|
|
34
|
+
* It supports dot notation, array notation, and mixed nested structures.
|
|
35
|
+
*
|
|
36
|
+
* @param formData - The FormData object to convert
|
|
37
|
+
* @returns A structured object representing the form data
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* Basic usage:
|
|
41
|
+
* ```ts
|
|
42
|
+
* const formData = new FormData();
|
|
43
|
+
* formData.append('name', 'John');
|
|
44
|
+
* formData.append('email', 'john@example.com');
|
|
45
|
+
*
|
|
46
|
+
* const result = formDataToObject(formData);
|
|
47
|
+
* // Result: { name: 'John', email: 'john@example.com' }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* Nested objects with dot notation:
|
|
52
|
+
* ```ts
|
|
53
|
+
* const formData = new FormData();
|
|
54
|
+
* formData.append('user.name', 'John');
|
|
55
|
+
* formData.append('user.profile.age', '30');
|
|
56
|
+
*
|
|
57
|
+
* const result = formDataToObject(formData);
|
|
58
|
+
* // Result: { user: { name: 'John', profile: { age: '30' } } }
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* Arrays with bracket notation:
|
|
63
|
+
* ```ts
|
|
64
|
+
* const formData = new FormData();
|
|
65
|
+
* formData.append('items[0]', 'apple');
|
|
66
|
+
* formData.append('items[1]', 'banana');
|
|
67
|
+
*
|
|
68
|
+
* const result = formDataToObject(formData);
|
|
69
|
+
* // Result: { items: ['apple', 'banana'] }
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* Multiple values for the same key:
|
|
74
|
+
* ```ts
|
|
75
|
+
* const formData = new FormData();
|
|
76
|
+
* formData.append('tags', 'javascript');
|
|
77
|
+
* formData.append('tags', 'typescript');
|
|
78
|
+
*
|
|
79
|
+
* const result = formDataToObject(formData);
|
|
80
|
+
* // Result: { tags: ['javascript', 'typescript'] }
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* Mixed nested structures:
|
|
85
|
+
* ```ts
|
|
86
|
+
* const formData = new FormData();
|
|
87
|
+
* formData.append('users[0].name', 'John');
|
|
88
|
+
* formData.append('users[0].emails[0]', 'john@work.com');
|
|
89
|
+
* formData.append('users[0].emails[1]', 'john@personal.com');
|
|
90
|
+
*
|
|
91
|
+
* const result = formDataToObject(formData);
|
|
92
|
+
* // Result: {
|
|
93
|
+
* // users: [{
|
|
94
|
+
* // name: 'John',
|
|
95
|
+
* // emails: ['john@work.com', 'john@personal.com']
|
|
96
|
+
* // }]
|
|
97
|
+
* // }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
function formDataToObject(formData) {
|
|
101
|
+
const obj = {};
|
|
102
|
+
for (const [key, value] of formData.entries()) set(obj, key.split(/[.[\]]/).filter(Boolean), value);
|
|
103
|
+
return obj;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
exports.formDataToObject = formDataToObject;
|
package/dist/utils.d.cts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region src/utils.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Converts FormData to a structured JavaScript object
|
|
4
|
+
*
|
|
5
|
+
* This function parses FormData entries and converts them into a nested object structure.
|
|
6
|
+
* It supports dot notation, array notation, and mixed nested structures.
|
|
7
|
+
*
|
|
8
|
+
* @param formData - The FormData object to convert
|
|
9
|
+
* @returns A structured object representing the form data
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* Basic usage:
|
|
13
|
+
* ```ts
|
|
14
|
+
* const formData = new FormData();
|
|
15
|
+
* formData.append('name', 'John');
|
|
16
|
+
* formData.append('email', 'john@example.com');
|
|
17
|
+
*
|
|
18
|
+
* const result = formDataToObject(formData);
|
|
19
|
+
* // Result: { name: 'John', email: 'john@example.com' }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* Nested objects with dot notation:
|
|
24
|
+
* ```ts
|
|
25
|
+
* const formData = new FormData();
|
|
26
|
+
* formData.append('user.name', 'John');
|
|
27
|
+
* formData.append('user.profile.age', '30');
|
|
28
|
+
*
|
|
29
|
+
* const result = formDataToObject(formData);
|
|
30
|
+
* // Result: { user: { name: 'John', profile: { age: '30' } } }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* Arrays with bracket notation:
|
|
35
|
+
* ```ts
|
|
36
|
+
* const formData = new FormData();
|
|
37
|
+
* formData.append('items[0]', 'apple');
|
|
38
|
+
* formData.append('items[1]', 'banana');
|
|
39
|
+
*
|
|
40
|
+
* const result = formDataToObject(formData);
|
|
41
|
+
* // Result: { items: ['apple', 'banana'] }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* Multiple values for the same key:
|
|
46
|
+
* ```ts
|
|
47
|
+
* const formData = new FormData();
|
|
48
|
+
* formData.append('tags', 'javascript');
|
|
49
|
+
* formData.append('tags', 'typescript');
|
|
50
|
+
*
|
|
51
|
+
* const result = formDataToObject(formData);
|
|
52
|
+
* // Result: { tags: ['javascript', 'typescript'] }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* Mixed nested structures:
|
|
57
|
+
* ```ts
|
|
58
|
+
* const formData = new FormData();
|
|
59
|
+
* formData.append('users[0].name', 'John');
|
|
60
|
+
* formData.append('users[0].emails[0]', 'john@work.com');
|
|
61
|
+
* formData.append('users[0].emails[1]', 'john@personal.com');
|
|
62
|
+
*
|
|
63
|
+
* const result = formDataToObject(formData);
|
|
64
|
+
* // Result: {
|
|
65
|
+
* // users: [{
|
|
66
|
+
* // name: 'John',
|
|
67
|
+
* // emails: ['john@work.com', 'john@personal.com']
|
|
68
|
+
* // }]
|
|
69
|
+
* // }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function formDataToObject(formData: FormData): Record<string, unknown>;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { formDataToObject };
|
package/dist/utils.d.mts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//#region src/utils.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Converts FormData to a structured JavaScript object
|
|
4
|
+
*
|
|
5
|
+
* This function parses FormData entries and converts them into a nested object structure.
|
|
6
|
+
* It supports dot notation, array notation, and mixed nested structures.
|
|
7
|
+
*
|
|
8
|
+
* @param formData - The FormData object to convert
|
|
9
|
+
* @returns A structured object representing the form data
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* Basic usage:
|
|
13
|
+
* ```ts
|
|
14
|
+
* const formData = new FormData();
|
|
15
|
+
* formData.append('name', 'John');
|
|
16
|
+
* formData.append('email', 'john@example.com');
|
|
17
|
+
*
|
|
18
|
+
* const result = formDataToObject(formData);
|
|
19
|
+
* // Result: { name: 'John', email: 'john@example.com' }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* Nested objects with dot notation:
|
|
24
|
+
* ```ts
|
|
25
|
+
* const formData = new FormData();
|
|
26
|
+
* formData.append('user.name', 'John');
|
|
27
|
+
* formData.append('user.profile.age', '30');
|
|
28
|
+
*
|
|
29
|
+
* const result = formDataToObject(formData);
|
|
30
|
+
* // Result: { user: { name: 'John', profile: { age: '30' } } }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* Arrays with bracket notation:
|
|
35
|
+
* ```ts
|
|
36
|
+
* const formData = new FormData();
|
|
37
|
+
* formData.append('items[0]', 'apple');
|
|
38
|
+
* formData.append('items[1]', 'banana');
|
|
39
|
+
*
|
|
40
|
+
* const result = formDataToObject(formData);
|
|
41
|
+
* // Result: { items: ['apple', 'banana'] }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* Multiple values for the same key:
|
|
46
|
+
* ```ts
|
|
47
|
+
* const formData = new FormData();
|
|
48
|
+
* formData.append('tags', 'javascript');
|
|
49
|
+
* formData.append('tags', 'typescript');
|
|
50
|
+
*
|
|
51
|
+
* const result = formDataToObject(formData);
|
|
52
|
+
* // Result: { tags: ['javascript', 'typescript'] }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* Mixed nested structures:
|
|
57
|
+
* ```ts
|
|
58
|
+
* const formData = new FormData();
|
|
59
|
+
* formData.append('users[0].name', 'John');
|
|
60
|
+
* formData.append('users[0].emails[0]', 'john@work.com');
|
|
61
|
+
* formData.append('users[0].emails[1]', 'john@personal.com');
|
|
62
|
+
*
|
|
63
|
+
* const result = formDataToObject(formData);
|
|
64
|
+
* // Result: {
|
|
65
|
+
* // users: [{
|
|
66
|
+
* // name: 'John',
|
|
67
|
+
* // emails: ['john@work.com', 'john@personal.com']
|
|
68
|
+
* // }]
|
|
69
|
+
* // }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function formDataToObject(formData: FormData): Record<string, unknown>;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { formDataToObject };
|
package/dist/utils.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//#region src/internal/assert.ts
|
|
2
|
+
function assert(condition, message) {
|
|
3
|
+
if (!condition) throw new Error(message || "Assertion failed");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/utils.ts
|
|
8
|
+
function isNumberString(str) {
|
|
9
|
+
return /^\d+$/.test(str);
|
|
10
|
+
}
|
|
11
|
+
function set(obj, path, value) {
|
|
12
|
+
if (path.length > 1) {
|
|
13
|
+
const newPath = [...path];
|
|
14
|
+
const key = newPath.shift();
|
|
15
|
+
assert(key != null);
|
|
16
|
+
const nextKey = newPath[0];
|
|
17
|
+
assert(nextKey != null);
|
|
18
|
+
if (!obj[key]) obj[key] = isNumberString(nextKey) ? [] : {};
|
|
19
|
+
else if (Array.isArray(obj[key]) && !isNumberString(nextKey)) obj[key] = Object.fromEntries(Object.entries(obj[key]));
|
|
20
|
+
set(obj[key], newPath, value);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const p = path[0];
|
|
24
|
+
assert(p != null);
|
|
25
|
+
if (obj[p] === void 0) obj[p] = value;
|
|
26
|
+
else if (Array.isArray(obj[p])) obj[p].push(value);
|
|
27
|
+
else obj[p] = [obj[p], value];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Converts FormData to a structured JavaScript object
|
|
31
|
+
*
|
|
32
|
+
* This function parses FormData entries and converts them into a nested object structure.
|
|
33
|
+
* It supports dot notation, array notation, and mixed nested structures.
|
|
34
|
+
*
|
|
35
|
+
* @param formData - The FormData object to convert
|
|
36
|
+
* @returns A structured object representing the form data
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* Basic usage:
|
|
40
|
+
* ```ts
|
|
41
|
+
* const formData = new FormData();
|
|
42
|
+
* formData.append('name', 'John');
|
|
43
|
+
* formData.append('email', 'john@example.com');
|
|
44
|
+
*
|
|
45
|
+
* const result = formDataToObject(formData);
|
|
46
|
+
* // Result: { name: 'John', email: 'john@example.com' }
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* Nested objects with dot notation:
|
|
51
|
+
* ```ts
|
|
52
|
+
* const formData = new FormData();
|
|
53
|
+
* formData.append('user.name', 'John');
|
|
54
|
+
* formData.append('user.profile.age', '30');
|
|
55
|
+
*
|
|
56
|
+
* const result = formDataToObject(formData);
|
|
57
|
+
* // Result: { user: { name: 'John', profile: { age: '30' } } }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* Arrays with bracket notation:
|
|
62
|
+
* ```ts
|
|
63
|
+
* const formData = new FormData();
|
|
64
|
+
* formData.append('items[0]', 'apple');
|
|
65
|
+
* formData.append('items[1]', 'banana');
|
|
66
|
+
*
|
|
67
|
+
* const result = formDataToObject(formData);
|
|
68
|
+
* // Result: { items: ['apple', 'banana'] }
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* Multiple values for the same key:
|
|
73
|
+
* ```ts
|
|
74
|
+
* const formData = new FormData();
|
|
75
|
+
* formData.append('tags', 'javascript');
|
|
76
|
+
* formData.append('tags', 'typescript');
|
|
77
|
+
*
|
|
78
|
+
* const result = formDataToObject(formData);
|
|
79
|
+
* // Result: { tags: ['javascript', 'typescript'] }
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* Mixed nested structures:
|
|
84
|
+
* ```ts
|
|
85
|
+
* const formData = new FormData();
|
|
86
|
+
* formData.append('users[0].name', 'John');
|
|
87
|
+
* formData.append('users[0].emails[0]', 'john@work.com');
|
|
88
|
+
* formData.append('users[0].emails[1]', 'john@personal.com');
|
|
89
|
+
*
|
|
90
|
+
* const result = formDataToObject(formData);
|
|
91
|
+
* // Result: {
|
|
92
|
+
* // users: [{
|
|
93
|
+
* // name: 'John',
|
|
94
|
+
* // emails: ['john@work.com', 'john@personal.com']
|
|
95
|
+
* // }]
|
|
96
|
+
* // }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
function formDataToObject(formData) {
|
|
100
|
+
const obj = {};
|
|
101
|
+
for (const [key, value] of formData.entries()) set(obj, key.split(/[.[\]]/).filter(Boolean), value);
|
|
102
|
+
return obj;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { formDataToObject };
|
package/package.json
CHANGED
|
@@ -1,48 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "server-act",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"action",
|
|
6
|
+
"next",
|
|
7
|
+
"nextjs",
|
|
8
|
+
"react",
|
|
9
|
+
"react server action",
|
|
10
|
+
"react server component",
|
|
11
|
+
"rsc",
|
|
12
|
+
"server action",
|
|
13
|
+
"server component"
|
|
14
|
+
],
|
|
4
15
|
"homepage": "https://github.com/chungweileong94/server-act#readme",
|
|
5
|
-
"
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/chungweileong94/server-act/issues"
|
|
18
|
+
},
|
|
6
19
|
"license": "MIT",
|
|
20
|
+
"author": "chungweileong94",
|
|
7
21
|
"repository": {
|
|
8
22
|
"type": "git",
|
|
9
23
|
"url": "git+https://github.com/chungweileong94/server-act.git"
|
|
10
24
|
},
|
|
11
|
-
"bugs": {
|
|
12
|
-
"url": "https://github.com/chungweileong94/server-act/issues"
|
|
13
|
-
},
|
|
14
|
-
"main": "dist/index.js",
|
|
15
|
-
"module": "dist/index.mjs",
|
|
16
|
-
"types": "./dist/index.d.ts",
|
|
17
|
-
"exports": {
|
|
18
|
-
".": {
|
|
19
|
-
"import": {
|
|
20
|
-
"types": "./dist/index.d.mts",
|
|
21
|
-
"default": "./dist/index.mjs"
|
|
22
|
-
},
|
|
23
|
-
"require": {
|
|
24
|
-
"types": "./dist/index.d.ts",
|
|
25
|
-
"default": "./dist/index.js"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
25
|
"files": [
|
|
30
26
|
"dist",
|
|
31
|
-
"package.json",
|
|
32
27
|
"LICENSE",
|
|
28
|
+
"package.json",
|
|
33
29
|
"README.md"
|
|
34
30
|
],
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.mjs",
|
|
35
|
+
"require": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./utils": {
|
|
38
|
+
"types": "./dist/utils.d.ts",
|
|
39
|
+
"import": "./dist/utils.mjs",
|
|
40
|
+
"require": "./dist/utils.js"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
46
44
|
"dependencies": {
|
|
47
45
|
"@standard-schema/spec": "^1.0.0",
|
|
48
46
|
"@standard-schema/utils": "^0.3.0"
|
package/dist/index.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
//#region rolldown:runtime
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
-
key = keys[i];
|
|
11
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
-
get: ((k) => from[k]).bind(null, key),
|
|
13
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
-
value: mod,
|
|
20
|
-
enumerable: true
|
|
21
|
-
}) : target, mod));
|
|
22
|
-
|
|
23
|
-
//#endregion
|
|
24
|
-
const __standard_schema_utils = __toESM(require("@standard-schema/utils"));
|
|
25
|
-
|
|
26
|
-
//#region src/utils.ts
|
|
27
|
-
async function standardValidate(schema, input) {
|
|
28
|
-
let result = schema["~standard"].validate(input);
|
|
29
|
-
if (result instanceof Promise) result = await result;
|
|
30
|
-
return result;
|
|
31
|
-
}
|
|
32
|
-
function getFormErrors(issues) {
|
|
33
|
-
const messages = [];
|
|
34
|
-
const fieldErrors = {};
|
|
35
|
-
for (const issue of issues) {
|
|
36
|
-
const dotPath = (0, __standard_schema_utils.getDotPath)(issue);
|
|
37
|
-
if (dotPath) if (fieldErrors[dotPath]) fieldErrors[dotPath].push(issue.message);
|
|
38
|
-
else fieldErrors[dotPath] = [issue.message];
|
|
39
|
-
else messages.push(issue.message);
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
messages,
|
|
43
|
-
fieldErrors
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
//#endregion
|
|
48
|
-
//#region src/index.ts
|
|
49
|
-
const unsetMarker = Symbol("unsetMarker");
|
|
50
|
-
function createNewServerActionBuilder(def) {
|
|
51
|
-
return createServerActionBuilder(def);
|
|
52
|
-
}
|
|
53
|
-
function createServerActionBuilder(initDef = {}) {
|
|
54
|
-
const _def = {
|
|
55
|
-
input: void 0,
|
|
56
|
-
middleware: void 0,
|
|
57
|
-
...initDef
|
|
58
|
-
};
|
|
59
|
-
return {
|
|
60
|
-
middleware: (middleware) => createNewServerActionBuilder({
|
|
61
|
-
..._def,
|
|
62
|
-
middleware
|
|
63
|
-
}),
|
|
64
|
-
input: (input) => createNewServerActionBuilder({
|
|
65
|
-
..._def,
|
|
66
|
-
input
|
|
67
|
-
}),
|
|
68
|
-
action: (action) => {
|
|
69
|
-
return async (input) => {
|
|
70
|
-
const ctx = await _def.middleware?.();
|
|
71
|
-
if (_def.input) {
|
|
72
|
-
const inputSchema = typeof _def.input === "function" ? await _def.input({ ctx }) : _def.input;
|
|
73
|
-
const result = await standardValidate(inputSchema, input);
|
|
74
|
-
if (result.issues) throw new __standard_schema_utils.SchemaError(result.issues);
|
|
75
|
-
return await action({
|
|
76
|
-
ctx,
|
|
77
|
-
input: result.value
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return await action({
|
|
81
|
-
ctx,
|
|
82
|
-
input: void 0
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
},
|
|
86
|
-
stateAction: (action) => {
|
|
87
|
-
return async (prevState, formData) => {
|
|
88
|
-
const ctx = await _def.middleware?.();
|
|
89
|
-
if (_def.input) {
|
|
90
|
-
const inputSchema = typeof _def.input === "function" ? await _def.input({ ctx }) : _def.input;
|
|
91
|
-
const result = await standardValidate(inputSchema, formData);
|
|
92
|
-
if (result.issues) return await action({
|
|
93
|
-
ctx,
|
|
94
|
-
prevState,
|
|
95
|
-
formData,
|
|
96
|
-
formErrors: getFormErrors(result.issues)
|
|
97
|
-
});
|
|
98
|
-
return await action({
|
|
99
|
-
ctx,
|
|
100
|
-
prevState,
|
|
101
|
-
formData,
|
|
102
|
-
input: result.value
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return await action({
|
|
106
|
-
ctx,
|
|
107
|
-
prevState,
|
|
108
|
-
formData,
|
|
109
|
-
input: void 0
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Server action builder
|
|
117
|
-
*/
|
|
118
|
-
const serverAct = createServerActionBuilder();
|
|
119
|
-
|
|
120
|
-
//#endregion
|
|
121
|
-
exports.serverAct = serverAct;
|