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 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
- import { zfd } from "zod-form-data";
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
- zfd.formData({
107
- name: zfd.text(
108
- z
109
- .string({ required_error: `You haven't told me your name` })
110
- .max(20, { message: "Any shorter name? You name is too long 😬" }),
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 ({ formData, input, formErrors, ctx }) => {
115
- if (formErrors) {
116
- return { formData, formErrors: formErrors.fieldErrors };
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
- ```tsx
123
- // client-component.tsx
124
- "use client";
161
+ ## Utilities
125
162
 
126
- import { useActionState } from "react";
127
- import { sayHelloAction } from "./action";
163
+ ### `formDataToObject`
128
164
 
129
- export const ClientComponent = () => {
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
- return (
133
- <form action={dispatch}>
134
- <input
135
- name="name"
136
- required
137
- defaultValue={state?.formData?.get("name")?.toString()}
138
- />
139
- {state?.formErrors?.name?.map((error) => <p key={error}>{error}</p>)}
140
-
141
- <button type="submit">Submit</button>
142
-
143
- {!!state?.message && <p>{state.message}</p>}
144
- </form>
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/utils.d.ts
4
- declare function getFormErrors(issues: ReadonlyArray<StandardSchemaV1.Issue>): {
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: <TContext>(middleware: () => Promise<TContext> | TContext) => Omit<ActionBuilder<{
34
+ middleware: <TNewContext>(middleware: MiddlewareFunction<RemoveUnsetMarker<TParams["_context"]>, TNewContext>) => ActionBuilder<{
28
35
  _input: TParams["_input"];
29
- _context: TContext;
30
- }>, "middleware">;
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: InferContextType<TParams["_context"]>;
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: InferContextType<TParams["_context"]>;
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: InferContextType<TParams["_context"]>;
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 getFormErrors>;
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/utils.d.ts
4
- declare function getFormErrors(issues: ReadonlyArray<StandardSchemaV1.Issue>): {
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: <TContext>(middleware: () => Promise<TContext> | TContext) => Omit<ActionBuilder<{
34
+ middleware: <TNewContext>(middleware: MiddlewareFunction<RemoveUnsetMarker<TParams["_context"]>, TNewContext>) => ActionBuilder<{
28
35
  _input: TParams["_input"];
29
- _context: TContext;
30
- }>, "middleware">;
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: InferContextType<TParams["_context"]>;
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: InferContextType<TParams["_context"]>;
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: InferContextType<TParams["_context"]>;
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 getFormErrors>;
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/utils.ts
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 getFormErrors(issues) {
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: void 0,
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
- const ctx = await _def.middleware?.();
63
+ let ctx = {};
64
+ if (_def.middleware.length > 0) ctx = await executeMiddlewares(_def.middleware, ctx);
48
65
  if (_def.input) {
49
- const inputSchema = typeof _def.input === "function" ? await _def.input({ ctx }) : _def.input;
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
- const ctx = await _def.middleware?.();
108
+ let ctx = {};
109
+ if (_def.middleware.length > 0) ctx = await executeMiddlewares(_def.middleware, ctx);
66
110
  if (_def.input) {
67
- const inputSchema = typeof _def.input === "function" ? await _def.input({ ctx }) : _def.input;
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: getFormErrors(result.issues)
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;
@@ -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 };
@@ -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.6.0",
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
- "author": "chungweileong94",
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
- "keywords": [
36
- "next",
37
- "nextjs",
38
- "react",
39
- "react server component",
40
- "react server action",
41
- "rsc",
42
- "server component",
43
- "server action",
44
- "action"
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;