@mhbdev/next-safe-route 0.0.37 → 0.0.38

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
@@ -6,7 +6,7 @@
6
6
  <a href="https://github.com/richardsolomou/next-safe-route/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/%40mhbdev%2Fnext-safe-route?style=for-the-badge" /></a>
7
7
  </p>
8
8
 
9
- `next-safe-route` is a utility library for Next.js that provides type-safety and schema validation for [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers)/API Routes. It is compatible with Next.js 15+ (including 16) route handler signatures.
9
+ `next-safe-route` is a utility library for Next.js that provides type-safety and schema validation for [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers), with an experimental server-actions layer. It is compatible with Next.js 15+ (including 16) route handler signatures.
10
10
 
11
11
  ## Features
12
12
 
@@ -14,6 +14,7 @@
14
14
  - **🧷 Type-Safe:** Work with full TypeScript type safety for parameters, query strings, and body content, including transformation results.
15
15
  - **🔗 Adapter-Friendly:** Ships with a zod (v4+) adapter by default and lazily loads optional adapters for valibot and yup.
16
16
  - **📦 Next-Ready:** Matches the Next.js Route Handler signature (including Next 15/16) and supports middleware-style context extensions.
17
+ - **⚡ Server Actions (Experimental):** Build safe server actions with input/output validation, middleware chaining, and React hooks.
17
18
  - **🧪 Fully Tested:** Extensive test suite to ensure everything works reliably.
18
19
 
19
20
  ## Installation
@@ -32,6 +33,12 @@ npm install valibot
32
33
  npm install yup
33
34
  ```
34
35
 
36
+ If you use the hooks subpath (`@mhbdev/next-safe-route/hooks`), install React in your project:
37
+
38
+ ```sh
39
+ npm install react react-dom
40
+ ```
41
+
35
42
  If an optional adapter is invoked without its peer dependency installed, a clear error message will explain what to install.
36
43
 
37
44
  ## Usage
@@ -71,7 +78,161 @@ To define a route handler in Next.js:
71
78
  1. Import `createSafeRoute` and your validation library (e.g., `zod`).
72
79
  2. Define validation schemas for params, query, and body as needed.
73
80
  3. Use `createSafeRoute()` to create a route handler, chaining `params`, `query`, and `body` methods.
74
- 4. Implement your handler function, accessing validated and type-safe params, query, and body through `context`. Body validation expects `Content-Type: application/json`.
81
+ 4. Implement your handler function, accessing validated and type-safe params, query, and body through `context`. Body validation supports `application/json`, `multipart/form-data`, and `application/x-www-form-urlencoded`.
82
+
83
+ ### Middleware context
84
+
85
+ Middlewares receive both the incoming request and the accumulated `context.data` from `baseContext` and previous middlewares. Middlewares can return either a context object or a `Response` (synchronously or asynchronously) to short-circuit execution.
86
+
87
+ ```ts
88
+ const GET = createSafeRoute({
89
+ baseContext: { tenantId: 'tenant-1' },
90
+ })
91
+ .use((request, data) => {
92
+ if (!request.headers.get('authorization')) {
93
+ return Response.json({ message: 'Unauthorized' }, { status: 401 });
94
+ }
95
+ return { userId: 'user-123', tenantId: data.tenantId };
96
+ })
97
+ .handler((request, context) => {
98
+ return Response.json(context.data);
99
+ });
100
+ ```
101
+
102
+ ### Parser options
103
+
104
+ You can customize query/body parsing behavior with `parserOptions`:
105
+
106
+ - `parserOptions.query.arrayStrategy`: `'auto' | 'always' | 'never'` (default: `'auto'`)
107
+ - `parserOptions.query.coerce`: `'none' | 'primitive' | ((value, key) => unknown)` (default: `'none'`)
108
+ - `parserOptions.body.strictContentType`: `boolean` (default: `true`)
109
+ - `parserOptions.body.allowEmptyBody`: `boolean` (default: `true`)
110
+ - `parserOptions.body.emptyValue`: value used when empty body is allowed (default: `{}`)
111
+ - `parserOptions.body.coerce`: `'none' | 'primitive' | ((value, key) => unknown)` for form/text values
112
+
113
+ ```ts
114
+ const POST = createSafeRoute({
115
+ parserOptions: {
116
+ query: {
117
+ arrayStrategy: 'always',
118
+ coerce: 'primitive',
119
+ },
120
+ body: {
121
+ strictContentType: false,
122
+ allowEmptyBody: false,
123
+ coerce: 'primitive',
124
+ },
125
+ },
126
+ })
127
+ .query(z.object({ page: z.array(z.number()) }))
128
+ .body(z.object({ count: z.number() }))
129
+ .handler((request, context) => {
130
+ return Response.json({ query: context.query, body: context.body });
131
+ });
132
+ ```
133
+
134
+ ## Server Actions (Experimental)
135
+
136
+ You can build non-throwing typed server actions with `createSafeActionClient`.
137
+
138
+ ```ts
139
+ // safe-action.ts
140
+ import { createSafeActionClient } from '@mhbdev/next-safe-route';
141
+
142
+ export const actionClient = createSafeActionClient({
143
+ defaultServerError: 'Something went wrong while executing the action.',
144
+ });
145
+ ```
146
+
147
+ ```ts
148
+ // greet-action.ts
149
+ 'use server';
150
+
151
+ import { z } from 'zod';
152
+ import { actionClient } from './safe-action';
153
+
154
+ export const greetAction = actionClient
155
+ .inputSchema(
156
+ z.object({
157
+ name: z.string().min(1),
158
+ }),
159
+ )
160
+ .action(async ({ parsedInput }) => {
161
+ return {
162
+ message: `Hello, ${parsedInput.name}!`,
163
+ };
164
+ });
165
+ ```
166
+
167
+ ### Action middleware (`next(...)` style)
168
+
169
+ Action middleware receives `parsedInput`, `ctx`, `metadata`, and `next`.
170
+
171
+ - Call `next()` to continue.
172
+ - Call `next({ ctx: { ... } })` to merge context for downstream middleware/handler.
173
+ - Return a `SafeActionResult` early to short-circuit.
174
+ - Calling `next()` more than once maps to a safe `serverError`.
175
+
176
+ ```ts
177
+ const action = actionClient
178
+ .inputSchema(z.object({ amount: z.number() }))
179
+ .use(async ({ ctx, next }) => {
180
+ if (!ctx.userId) {
181
+ return { serverError: 'Unauthorized' };
182
+ }
183
+
184
+ return next({
185
+ ctx: {
186
+ requestId: crypto.randomUUID(),
187
+ },
188
+ });
189
+ })
190
+ .action(async ({ parsedInput, ctx }) => {
191
+ return {
192
+ ok: true,
193
+ amount: parsedInput.amount,
194
+ requestId: ctx.requestId as string,
195
+ };
196
+ });
197
+ ```
198
+
199
+ ### Result contract
200
+
201
+ Actions always resolve to a non-throwing result envelope:
202
+
203
+ - Success: `{ data }`
204
+ - Input validation failure: `{ validationErrors: { fieldErrors, formErrors } }`
205
+ - Server failure: `{ serverError }`
206
+
207
+ Validation paths are normalized to dot keys (for example `users.0.name`).
208
+
209
+ ### Hooks (`@mhbdev/next-safe-route/hooks`)
210
+
211
+ ```ts
212
+ 'use client';
213
+
214
+ import { useAction } from '@mhbdev/next-safe-route/hooks';
215
+ import { greetAction } from './greet-action';
216
+
217
+ export function Greet() {
218
+ const { execute, result, status, reset } = useAction(greetAction);
219
+
220
+ return (
221
+ <div>
222
+ <button onClick={() => execute({ name: 'John Doe' })}>Run</button>
223
+ <button onClick={reset}>Reset</button>
224
+ <pre>{JSON.stringify({ status, result }, null, 2)}</pre>
225
+ </div>
226
+ );
227
+ }
228
+ ```
229
+
230
+ Additional hooks:
231
+
232
+ - `useOptimisticAction(action, { initialState, updateFn, preserveOnError? })`
233
+ - `useStateAction(action, { initialState, mapFormData?, onSuccess?, onValidationError?, onServerError? })`
234
+
235
+ `useStateAction` includes `formAction(formData)` for direct `<form action={formAction}>` usage.
75
236
 
76
237
  ### Using other validation libraries
77
238
 
@@ -0,0 +1,71 @@
1
+ import { S as SafeActionValidationErrors, a as SafeActionResult } from './safeActionTypes-DfvihJur.mjs';
2
+ import './types-DYbZEItT.mjs';
3
+ import '@sinclair/typebox';
4
+ import 'valibot';
5
+ import 'yup';
6
+ import 'zod';
7
+
8
+ type AnyAction$2 = (...args: never[]) => Promise<SafeActionResult<unknown>>;
9
+ type ActionResult$2<TAction extends AnyAction$2> = Awaited<ReturnType<TAction>>;
10
+ type ActionData$1<TAction extends AnyAction$2> = ActionResult$2<TAction> extends SafeActionResult<infer TData> ? TData : never;
11
+ type SafeActionStatus = 'idle' | 'executing' | 'success' | 'validation-error' | 'server-error';
12
+ type UseActionOptions<TAction extends AnyAction$2> = {
13
+ onSuccess?: (data: ActionData$1<TAction>) => void;
14
+ onValidationError?: (validationErrors: SafeActionValidationErrors) => void;
15
+ onServerError?: (serverError: string) => void;
16
+ onSettled?: (result: ActionResult$2<TAction>) => void;
17
+ };
18
+ declare function useAction<TAction extends AnyAction$2>(action: TAction, options?: UseActionOptions<TAction>): {
19
+ execute: (...args: Parameters<TAction>) => void;
20
+ executeAsync: (...args: Parameters<TAction>) => Promise<ActionResult$2<TAction>>;
21
+ result: Awaited<ReturnType<TAction>> | undefined;
22
+ status: SafeActionStatus;
23
+ isPending: boolean;
24
+ hasExecuted: boolean;
25
+ reset: () => void;
26
+ };
27
+
28
+ type AnyAction$1 = (...args: never[]) => Promise<SafeActionResult<unknown>>;
29
+ type ActionResult$1<TAction extends AnyAction$1> = Awaited<ReturnType<TAction>>;
30
+ type ActionInput$1<TAction extends AnyAction$1> = Parameters<TAction> extends [] ? void : Parameters<TAction>[0];
31
+ type UseOptimisticActionOptions<TAction extends AnyAction$1, TOptimisticState> = UseActionOptions<TAction> & {
32
+ initialState: TOptimisticState;
33
+ updateFn: (currentState: TOptimisticState, input: ActionInput$1<TAction>) => TOptimisticState;
34
+ preserveOnError?: boolean;
35
+ };
36
+ declare function useOptimisticAction<TAction extends AnyAction$1, TOptimisticState>(action: TAction, options: UseOptimisticActionOptions<TAction, TOptimisticState>): {
37
+ execute: (...args: Parameters<TAction>) => void;
38
+ executeAsync: (...args: Parameters<TAction>) => Promise<ActionResult$1<TAction>>;
39
+ result: Awaited<ReturnType<TAction>> | undefined;
40
+ status: SafeActionStatus;
41
+ isPending: boolean;
42
+ hasExecuted: boolean;
43
+ reset: () => void;
44
+ optimisticState: TOptimisticState;
45
+ };
46
+
47
+ type AnyAction = (...args: never[]) => Promise<SafeActionResult<unknown>>;
48
+ type ActionResult<TAction extends AnyAction> = Awaited<ReturnType<TAction>>;
49
+ type ActionInput<TAction extends AnyAction> = Parameters<TAction> extends [] ? void : Parameters<TAction>[0];
50
+ type ActionData<TAction extends AnyAction> = ActionResult<TAction> extends SafeActionResult<infer TData> ? TData : never;
51
+ type UseStateActionOptions<TAction extends AnyAction, TState> = {
52
+ initialState: TState;
53
+ mapFormData?: (formData: FormData) => ActionInput<TAction>;
54
+ onSuccess?: (prevState: TState, data: ActionData<TAction>) => TState;
55
+ onValidationError?: (prevState: TState, validationErrors: SafeActionValidationErrors) => TState;
56
+ onServerError?: (prevState: TState, serverError: string) => TState;
57
+ onSettled?: (result: ActionResult<TAction>) => void;
58
+ };
59
+ declare function useStateAction<TAction extends AnyAction, TState>(action: TAction, options: UseStateActionOptions<TAction, TState>): {
60
+ formAction: (formData: FormData) => Promise<ReturnType<TAction>>;
61
+ execute: (...args: Parameters<TAction>) => void;
62
+ executeAsync: (...args: Parameters<TAction>) => Promise<Awaited<ReturnType<TAction>>>;
63
+ result: Awaited<ReturnType<TAction>> | undefined;
64
+ status: SafeActionStatus;
65
+ isPending: boolean;
66
+ hasExecuted: boolean;
67
+ reset: () => void;
68
+ state: TState;
69
+ };
70
+
71
+ export { type SafeActionStatus, type UseActionOptions, type UseOptimisticActionOptions, type UseStateActionOptions, useAction, useOptimisticAction, useStateAction };
@@ -0,0 +1,71 @@
1
+ import { S as SafeActionValidationErrors, a as SafeActionResult } from './safeActionTypes-ClmL2Zxu.js';
2
+ import './types-DYbZEItT.js';
3
+ import '@sinclair/typebox';
4
+ import 'valibot';
5
+ import 'yup';
6
+ import 'zod';
7
+
8
+ type AnyAction$2 = (...args: never[]) => Promise<SafeActionResult<unknown>>;
9
+ type ActionResult$2<TAction extends AnyAction$2> = Awaited<ReturnType<TAction>>;
10
+ type ActionData$1<TAction extends AnyAction$2> = ActionResult$2<TAction> extends SafeActionResult<infer TData> ? TData : never;
11
+ type SafeActionStatus = 'idle' | 'executing' | 'success' | 'validation-error' | 'server-error';
12
+ type UseActionOptions<TAction extends AnyAction$2> = {
13
+ onSuccess?: (data: ActionData$1<TAction>) => void;
14
+ onValidationError?: (validationErrors: SafeActionValidationErrors) => void;
15
+ onServerError?: (serverError: string) => void;
16
+ onSettled?: (result: ActionResult$2<TAction>) => void;
17
+ };
18
+ declare function useAction<TAction extends AnyAction$2>(action: TAction, options?: UseActionOptions<TAction>): {
19
+ execute: (...args: Parameters<TAction>) => void;
20
+ executeAsync: (...args: Parameters<TAction>) => Promise<ActionResult$2<TAction>>;
21
+ result: Awaited<ReturnType<TAction>> | undefined;
22
+ status: SafeActionStatus;
23
+ isPending: boolean;
24
+ hasExecuted: boolean;
25
+ reset: () => void;
26
+ };
27
+
28
+ type AnyAction$1 = (...args: never[]) => Promise<SafeActionResult<unknown>>;
29
+ type ActionResult$1<TAction extends AnyAction$1> = Awaited<ReturnType<TAction>>;
30
+ type ActionInput$1<TAction extends AnyAction$1> = Parameters<TAction> extends [] ? void : Parameters<TAction>[0];
31
+ type UseOptimisticActionOptions<TAction extends AnyAction$1, TOptimisticState> = UseActionOptions<TAction> & {
32
+ initialState: TOptimisticState;
33
+ updateFn: (currentState: TOptimisticState, input: ActionInput$1<TAction>) => TOptimisticState;
34
+ preserveOnError?: boolean;
35
+ };
36
+ declare function useOptimisticAction<TAction extends AnyAction$1, TOptimisticState>(action: TAction, options: UseOptimisticActionOptions<TAction, TOptimisticState>): {
37
+ execute: (...args: Parameters<TAction>) => void;
38
+ executeAsync: (...args: Parameters<TAction>) => Promise<ActionResult$1<TAction>>;
39
+ result: Awaited<ReturnType<TAction>> | undefined;
40
+ status: SafeActionStatus;
41
+ isPending: boolean;
42
+ hasExecuted: boolean;
43
+ reset: () => void;
44
+ optimisticState: TOptimisticState;
45
+ };
46
+
47
+ type AnyAction = (...args: never[]) => Promise<SafeActionResult<unknown>>;
48
+ type ActionResult<TAction extends AnyAction> = Awaited<ReturnType<TAction>>;
49
+ type ActionInput<TAction extends AnyAction> = Parameters<TAction> extends [] ? void : Parameters<TAction>[0];
50
+ type ActionData<TAction extends AnyAction> = ActionResult<TAction> extends SafeActionResult<infer TData> ? TData : never;
51
+ type UseStateActionOptions<TAction extends AnyAction, TState> = {
52
+ initialState: TState;
53
+ mapFormData?: (formData: FormData) => ActionInput<TAction>;
54
+ onSuccess?: (prevState: TState, data: ActionData<TAction>) => TState;
55
+ onValidationError?: (prevState: TState, validationErrors: SafeActionValidationErrors) => TState;
56
+ onServerError?: (prevState: TState, serverError: string) => TState;
57
+ onSettled?: (result: ActionResult<TAction>) => void;
58
+ };
59
+ declare function useStateAction<TAction extends AnyAction, TState>(action: TAction, options: UseStateActionOptions<TAction, TState>): {
60
+ formAction: (formData: FormData) => Promise<ReturnType<TAction>>;
61
+ execute: (...args: Parameters<TAction>) => void;
62
+ executeAsync: (...args: Parameters<TAction>) => Promise<Awaited<ReturnType<TAction>>>;
63
+ result: Awaited<ReturnType<TAction>> | undefined;
64
+ status: SafeActionStatus;
65
+ isPending: boolean;
66
+ hasExecuted: boolean;
67
+ reset: () => void;
68
+ state: TState;
69
+ };
70
+
71
+ export { type SafeActionStatus, type UseActionOptions, type UseOptimisticActionOptions, type UseStateActionOptions, useAction, useOptimisticAction, useStateAction };
package/dist/hooks.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var x=Object.defineProperty;var D=Object.getOwnPropertyDescriptor;var P=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var w=(n,t)=>{for(var u in t)x(n,u,{get:t[u],enumerable:!0})},V=(n,t,u,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let e of P(t))!g.call(n,e)&&e!==u&&x(n,e,{get:()=>t[e],enumerable:!(o=D(t,e))||o.enumerable});return n};var k=n=>V(x({},"__esModule",{value:!0}),n);var F={};w(F,{useAction:()=>T,useOptimisticAction:()=>E,useStateAction:()=>R});module.exports=k(F);var d=require("react");function U(n){return"data"in n?"success":"validationErrors"in n?"validation-error":"server-error"}function T(n,t){let[u,o]=(0,d.useState)(void 0),[e,i]=(0,d.useState)("idle"),[p,c]=(0,d.useState)(!1),[r,S]=(0,d.useTransition)(),l=(0,d.useCallback)(async(...f)=>{i("executing"),c(!0);let s=await n(...f);o(s);let O=U(s);if(i(O),"data"in s)t?.onSuccess?.(s.data);else if("validationErrors"in s){let v=s.validationErrors;v&&t?.onValidationError?.(v)}else"serverError"in s&&t?.onServerError?.(s.serverError);return t?.onSettled?.(s),s},[n,t]),y=(0,d.useCallback)((...f)=>{S(()=>{l(...f)})},[l,S]),a=(0,d.useCallback)(()=>{i("idle"),o(void 0),c(!1)},[]);return{execute:y,executeAsync:l,result:u,status:e,isPending:r,hasExecuted:p,reset:a}}var A=require("react");function E(n,t){let[u,o]=(0,A.useState)(t.initialState),e=(0,A.useRef)(t.initialState),i=(0,A.useRef)(void 0),[p,c]=(0,A.useTransition)(),r=T(n,{...t,onSuccess:a=>{i.current=void 0,t.onSuccess?.(a)},onValidationError:a=>{!t.preserveOnError&&i.current!==void 0&&(e.current=i.current,o(i.current)),i.current=void 0,t.onValidationError?.(a)},onServerError:a=>{!t.preserveOnError&&i.current!==void 0&&(e.current=i.current,o(i.current)),i.current=void 0,t.onServerError?.(a)},onSettled:a=>{t.onSettled?.(a)}}),S=(0,A.useCallback)(async(...a)=>{let f=a[0];i.current=e.current;let s=t.updateFn(e.current,f);return e.current=s,o(s),await r.executeAsync(...a)},[r,t]),l=(0,A.useCallback)((...a)=>{c(()=>{S(...a)})},[S,c]),y=(0,A.useCallback)(()=>{e.current=t.initialState,i.current=void 0,o(t.initialState),r.reset()},[r,t.initialState]);return{execute:l,executeAsync:S,result:r.result,status:r.status,isPending:r.isPending||p,hasExecuted:r.hasExecuted,reset:y,optimisticState:u}}var m=require("react");function R(n,t){let[u,o]=(0,m.useState)(t.initialState),e=T(n,{onSuccess:c=>{t.onSuccess&&o(r=>t.onSuccess(r,c))},onValidationError:c=>{t.onValidationError&&o(r=>t.onValidationError(r,c))},onServerError:c=>{t.onServerError&&o(r=>t.onServerError(r,c))},onSettled:c=>{t.onSettled?.(c)}}),i=(0,m.useCallback)(async c=>{let S=(t.mapFormData??(l=>Object.fromEntries(l.entries())))(c);return e.executeAsync(S)},[e,t]),p=(0,m.useCallback)(()=>{o(t.initialState),e.reset()},[e,t.initialState]);return{formAction:i,execute:e.execute,executeAsync:e.executeAsync,result:e.result,status:e.status,isPending:e.isPending,hasExecuted:e.hasExecuted,reset:p,state:u}}0&&(module.exports={useAction,useOptimisticAction,useStateAction});
2
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks.ts","../src/hooks/useAction.ts","../src/hooks/useOptimisticAction.ts","../src/hooks/useStateAction.ts"],"sourcesContent":["export * from './hooks/index';\n","import { useCallback, useState, useTransition } from 'react';\n\nimport type { SafeActionResult, SafeActionValidationErrors } from '../safeActionTypes';\n\ntype AnyAction = (...args: never[]) => Promise<SafeActionResult<unknown>>;\n\ntype ActionResult<TAction extends AnyAction> = Awaited<ReturnType<TAction>>;\n\ntype ActionData<TAction extends AnyAction> =\n ActionResult<TAction> extends SafeActionResult<infer TData> ? TData : never;\n\nexport type SafeActionStatus = 'idle' | 'executing' | 'success' | 'validation-error' | 'server-error';\n\nexport type UseActionOptions<TAction extends AnyAction> = {\n onSuccess?: (data: ActionData<TAction>) => void;\n onValidationError?: (validationErrors: SafeActionValidationErrors) => void;\n onServerError?: (serverError: string) => void;\n onSettled?: (result: ActionResult<TAction>) => void;\n};\n\nfunction getStatusFromResult(result: SafeActionResult<unknown>): SafeActionStatus {\n if ('data' in result) {\n return 'success';\n }\n\n if ('validationErrors' in result) {\n return 'validation-error';\n }\n\n return 'server-error';\n}\n\nexport function useAction<TAction extends AnyAction>(action: TAction, options?: UseActionOptions<TAction>) {\n const [result, setResult] = useState<ActionResult<TAction> | undefined>(undefined);\n const [status, setStatus] = useState<SafeActionStatus>('idle');\n const [hasExecuted, setHasExecuted] = useState(false);\n const [isPending, startTransition] = useTransition();\n\n const executeAsync = useCallback(\n async (...args: Parameters<TAction>): Promise<ActionResult<TAction>> => {\n setStatus('executing');\n setHasExecuted(true);\n\n const actionResult = (await action(...args)) as ActionResult<TAction>;\n setResult(actionResult);\n\n const nextStatus = getStatusFromResult(actionResult as SafeActionResult<unknown>);\n setStatus(nextStatus);\n\n if ('data' in actionResult) {\n options?.onSuccess?.(actionResult.data as ActionData<TAction>);\n } else if ('validationErrors' in actionResult) {\n const validationErrors = actionResult.validationErrors;\n if (validationErrors) {\n options?.onValidationError?.(validationErrors);\n }\n } else if ('serverError' in actionResult) {\n options?.onServerError?.(actionResult.serverError);\n }\n\n options?.onSettled?.(actionResult);\n return actionResult;\n },\n [action, options],\n );\n\n const execute = useCallback(\n (...args: Parameters<TAction>) => {\n startTransition(() => {\n void executeAsync(...args);\n });\n },\n [executeAsync, startTransition],\n );\n\n const reset = useCallback(() => {\n setStatus('idle');\n setResult(undefined);\n setHasExecuted(false);\n }, []);\n\n return {\n execute,\n executeAsync,\n result,\n status,\n isPending,\n hasExecuted,\n reset,\n };\n}\n","import { useCallback, useRef, useState, useTransition } from 'react';\n\nimport type { SafeActionResult } from '../safeActionTypes';\nimport { type UseActionOptions, useAction } from './useAction';\n\ntype AnyAction = (...args: never[]) => Promise<SafeActionResult<unknown>>;\n\ntype ActionResult<TAction extends AnyAction> = Awaited<ReturnType<TAction>>;\n\ntype ActionInput<TAction extends AnyAction> = Parameters<TAction> extends [] ? void : Parameters<TAction>[0];\n\ntype ActionData<TAction extends AnyAction> =\n ActionResult<TAction> extends SafeActionResult<infer TData> ? TData : never;\n\nexport type UseOptimisticActionOptions<TAction extends AnyAction, TOptimisticState> = UseActionOptions<TAction> & {\n initialState: TOptimisticState;\n updateFn: (currentState: TOptimisticState, input: ActionInput<TAction>) => TOptimisticState;\n preserveOnError?: boolean;\n};\n\nexport function useOptimisticAction<TAction extends AnyAction, TOptimisticState>(\n action: TAction,\n options: UseOptimisticActionOptions<TAction, TOptimisticState>,\n) {\n const [optimisticState, setOptimisticState] = useState(options.initialState);\n const optimisticStateRef = useRef(options.initialState);\n const rollbackStateRef = useRef<TOptimisticState | undefined>(undefined);\n const [isPendingTransition, startTransition] = useTransition();\n\n const actionState = useAction(action, {\n ...options,\n onSuccess: (data) => {\n rollbackStateRef.current = undefined;\n options.onSuccess?.(data as ActionData<TAction>);\n },\n onValidationError: (validationErrors) => {\n if (!options.preserveOnError && rollbackStateRef.current !== undefined) {\n optimisticStateRef.current = rollbackStateRef.current;\n setOptimisticState(rollbackStateRef.current);\n }\n rollbackStateRef.current = undefined;\n options.onValidationError?.(validationErrors);\n },\n onServerError: (serverError) => {\n if (!options.preserveOnError && rollbackStateRef.current !== undefined) {\n optimisticStateRef.current = rollbackStateRef.current;\n setOptimisticState(rollbackStateRef.current);\n }\n rollbackStateRef.current = undefined;\n options.onServerError?.(serverError);\n },\n onSettled: (result) => {\n options.onSettled?.(result as ActionResult<TAction>);\n },\n });\n\n const executeAsync = useCallback(\n async (...args: Parameters<TAction>): Promise<ActionResult<TAction>> => {\n const input = args[0] as ActionInput<TAction>;\n rollbackStateRef.current = optimisticStateRef.current;\n\n const nextOptimisticState = options.updateFn(optimisticStateRef.current, input);\n optimisticStateRef.current = nextOptimisticState;\n setOptimisticState(nextOptimisticState);\n\n return (await actionState.executeAsync(...args)) as ActionResult<TAction>;\n },\n [actionState, options],\n );\n\n const execute = useCallback(\n (...args: Parameters<TAction>) => {\n startTransition(() => {\n void executeAsync(...args);\n });\n },\n [executeAsync, startTransition],\n );\n\n const reset = useCallback(() => {\n optimisticStateRef.current = options.initialState;\n rollbackStateRef.current = undefined;\n setOptimisticState(options.initialState);\n actionState.reset();\n }, [actionState, options.initialState]);\n\n return {\n execute,\n executeAsync,\n result: actionState.result,\n status: actionState.status,\n isPending: actionState.isPending || isPendingTransition,\n hasExecuted: actionState.hasExecuted,\n reset,\n optimisticState,\n };\n}\n","import { useCallback, useState } from 'react';\n\nimport type { SafeActionResult, SafeActionValidationErrors } from '../safeActionTypes';\nimport { useAction } from './useAction';\n\ntype AnyAction = (...args: never[]) => Promise<SafeActionResult<unknown>>;\n\ntype ActionResult<TAction extends AnyAction> = Awaited<ReturnType<TAction>>;\n\ntype ActionInput<TAction extends AnyAction> = Parameters<TAction> extends [] ? void : Parameters<TAction>[0];\n\ntype ActionData<TAction extends AnyAction> =\n ActionResult<TAction> extends SafeActionResult<infer TData> ? TData : never;\n\nexport type UseStateActionOptions<TAction extends AnyAction, TState> = {\n initialState: TState;\n mapFormData?: (formData: FormData) => ActionInput<TAction>;\n onSuccess?: (prevState: TState, data: ActionData<TAction>) => TState;\n onValidationError?: (prevState: TState, validationErrors: SafeActionValidationErrors) => TState;\n onServerError?: (prevState: TState, serverError: string) => TState;\n onSettled?: (result: ActionResult<TAction>) => void;\n};\n\nexport function useStateAction<TAction extends AnyAction, TState>(\n action: TAction,\n options: UseStateActionOptions<TAction, TState>,\n) {\n const [state, setState] = useState(options.initialState);\n\n const actionState = useAction(action, {\n onSuccess: (data) => {\n if (options.onSuccess) {\n setState((prevState) => options.onSuccess!(prevState, data as ActionData<TAction>));\n }\n },\n onValidationError: (validationErrors) => {\n if (options.onValidationError) {\n setState((prevState) => options.onValidationError!(prevState, validationErrors));\n }\n },\n onServerError: (serverError) => {\n if (options.onServerError) {\n setState((prevState) => options.onServerError!(prevState, serverError));\n }\n },\n onSettled: (result) => {\n options.onSettled?.(result as ActionResult<TAction>);\n },\n });\n\n const formAction = useCallback(\n async (formData: FormData) => {\n const mapper =\n options.mapFormData ??\n ((value: FormData) => Object.fromEntries(value.entries()) as unknown as ActionInput<TAction>);\n const mappedInput = mapper(formData);\n return (actionState.executeAsync as (...args: unknown[]) => Promise<ActionResult<TAction>>)(mappedInput);\n },\n [actionState, options],\n );\n\n const reset = useCallback(() => {\n setState(options.initialState);\n actionState.reset();\n }, [actionState, options.initialState]);\n\n return {\n formAction,\n execute: actionState.execute,\n executeAsync: actionState.executeAsync,\n result: actionState.result,\n status: actionState.status,\n isPending: actionState.isPending,\n hasExecuted: actionState.hasExecuted,\n reset,\n state,\n };\n}\n"],"mappings":"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","names":["hooks_exports","__export","useAction","useOptimisticAction","useStateAction","__toCommonJS","import_react","getStatusFromResult","result","useAction","action","options","setResult","status","setStatus","hasExecuted","setHasExecuted","isPending","startTransition","executeAsync","args","actionResult","nextStatus","validationErrors","execute","reset","import_react","useOptimisticAction","action","options","optimisticState","setOptimisticState","optimisticStateRef","rollbackStateRef","isPendingTransition","startTransition","actionState","useAction","data","validationErrors","serverError","result","executeAsync","args","input","nextOptimisticState","execute","reset","import_react","useStateAction","action","options","state","setState","actionState","useAction","data","prevState","validationErrors","serverError","result","formAction","formData","mappedInput","value","reset"]}
package/dist/hooks.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{useCallback as f,useState as m,useTransition as O}from"react";function D(s){return"data"in s?"success":"validationErrors"in s?"validation-error":"server-error"}function T(s,t){let[d,a]=m(void 0),[e,n]=m("idle"),[S,r]=m(!1),[i,A]=O(),u=f(async(...l)=>{n("executing"),r(!0);let c=await s(...l);a(c);let R=D(c);if(n(R),"data"in c)t?.onSuccess?.(c.data);else if("validationErrors"in c){let x=c.validationErrors;x&&t?.onValidationError?.(x)}else"serverError"in c&&t?.onServerError?.(c.serverError);return t?.onSettled?.(c),c},[s,t]),p=f((...l)=>{A(()=>{u(...l)})},[u,A]),o=f(()=>{n("idle"),a(void 0),r(!1)},[]);return{execute:p,executeAsync:u,result:d,status:e,isPending:i,hasExecuted:S,reset:o}}import{useCallback as y,useRef as v,useState as P,useTransition as g}from"react";function w(s,t){let[d,a]=P(t.initialState),e=v(t.initialState),n=v(void 0),[S,r]=g(),i=T(s,{...t,onSuccess:o=>{n.current=void 0,t.onSuccess?.(o)},onValidationError:o=>{!t.preserveOnError&&n.current!==void 0&&(e.current=n.current,a(n.current)),n.current=void 0,t.onValidationError?.(o)},onServerError:o=>{!t.preserveOnError&&n.current!==void 0&&(e.current=n.current,a(n.current)),n.current=void 0,t.onServerError?.(o)},onSettled:o=>{t.onSettled?.(o)}}),A=y(async(...o)=>{let l=o[0];n.current=e.current;let c=t.updateFn(e.current,l);return e.current=c,a(c),await i.executeAsync(...o)},[i,t]),u=y((...o)=>{r(()=>{A(...o)})},[A,r]),p=y(()=>{e.current=t.initialState,n.current=void 0,a(t.initialState),i.reset()},[i,t.initialState]);return{execute:u,executeAsync:A,result:i.result,status:i.status,isPending:i.isPending||S,hasExecuted:i.hasExecuted,reset:p,optimisticState:d}}import{useCallback as E,useState as V}from"react";function k(s,t){let[d,a]=V(t.initialState),e=T(s,{onSuccess:r=>{t.onSuccess&&a(i=>t.onSuccess(i,r))},onValidationError:r=>{t.onValidationError&&a(i=>t.onValidationError(i,r))},onServerError:r=>{t.onServerError&&a(i=>t.onServerError(i,r))},onSettled:r=>{t.onSettled?.(r)}}),n=E(async r=>{let A=(t.mapFormData??(u=>Object.fromEntries(u.entries())))(r);return e.executeAsync(A)},[e,t]),S=E(()=>{a(t.initialState),e.reset()},[e,t.initialState]);return{formAction:n,execute:e.execute,executeAsync:e.executeAsync,result:e.result,status:e.status,isPending:e.isPending,hasExecuted:e.hasExecuted,reset:S,state:d}}export{T as useAction,w as useOptimisticAction,k as useStateAction};
2
+ //# sourceMappingURL=hooks.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/useAction.ts","../src/hooks/useOptimisticAction.ts","../src/hooks/useStateAction.ts"],"sourcesContent":["import { useCallback, useState, useTransition } from 'react';\n\nimport type { SafeActionResult, SafeActionValidationErrors } from '../safeActionTypes';\n\ntype AnyAction = (...args: never[]) => Promise<SafeActionResult<unknown>>;\n\ntype ActionResult<TAction extends AnyAction> = Awaited<ReturnType<TAction>>;\n\ntype ActionData<TAction extends AnyAction> =\n ActionResult<TAction> extends SafeActionResult<infer TData> ? TData : never;\n\nexport type SafeActionStatus = 'idle' | 'executing' | 'success' | 'validation-error' | 'server-error';\n\nexport type UseActionOptions<TAction extends AnyAction> = {\n onSuccess?: (data: ActionData<TAction>) => void;\n onValidationError?: (validationErrors: SafeActionValidationErrors) => void;\n onServerError?: (serverError: string) => void;\n onSettled?: (result: ActionResult<TAction>) => void;\n};\n\nfunction getStatusFromResult(result: SafeActionResult<unknown>): SafeActionStatus {\n if ('data' in result) {\n return 'success';\n }\n\n if ('validationErrors' in result) {\n return 'validation-error';\n }\n\n return 'server-error';\n}\n\nexport function useAction<TAction extends AnyAction>(action: TAction, options?: UseActionOptions<TAction>) {\n const [result, setResult] = useState<ActionResult<TAction> | undefined>(undefined);\n const [status, setStatus] = useState<SafeActionStatus>('idle');\n const [hasExecuted, setHasExecuted] = useState(false);\n const [isPending, startTransition] = useTransition();\n\n const executeAsync = useCallback(\n async (...args: Parameters<TAction>): Promise<ActionResult<TAction>> => {\n setStatus('executing');\n setHasExecuted(true);\n\n const actionResult = (await action(...args)) as ActionResult<TAction>;\n setResult(actionResult);\n\n const nextStatus = getStatusFromResult(actionResult as SafeActionResult<unknown>);\n setStatus(nextStatus);\n\n if ('data' in actionResult) {\n options?.onSuccess?.(actionResult.data as ActionData<TAction>);\n } else if ('validationErrors' in actionResult) {\n const validationErrors = actionResult.validationErrors;\n if (validationErrors) {\n options?.onValidationError?.(validationErrors);\n }\n } else if ('serverError' in actionResult) {\n options?.onServerError?.(actionResult.serverError);\n }\n\n options?.onSettled?.(actionResult);\n return actionResult;\n },\n [action, options],\n );\n\n const execute = useCallback(\n (...args: Parameters<TAction>) => {\n startTransition(() => {\n void executeAsync(...args);\n });\n },\n [executeAsync, startTransition],\n );\n\n const reset = useCallback(() => {\n setStatus('idle');\n setResult(undefined);\n setHasExecuted(false);\n }, []);\n\n return {\n execute,\n executeAsync,\n result,\n status,\n isPending,\n hasExecuted,\n reset,\n };\n}\n","import { useCallback, useRef, useState, useTransition } from 'react';\n\nimport type { SafeActionResult } from '../safeActionTypes';\nimport { type UseActionOptions, useAction } from './useAction';\n\ntype AnyAction = (...args: never[]) => Promise<SafeActionResult<unknown>>;\n\ntype ActionResult<TAction extends AnyAction> = Awaited<ReturnType<TAction>>;\n\ntype ActionInput<TAction extends AnyAction> = Parameters<TAction> extends [] ? void : Parameters<TAction>[0];\n\ntype ActionData<TAction extends AnyAction> =\n ActionResult<TAction> extends SafeActionResult<infer TData> ? TData : never;\n\nexport type UseOptimisticActionOptions<TAction extends AnyAction, TOptimisticState> = UseActionOptions<TAction> & {\n initialState: TOptimisticState;\n updateFn: (currentState: TOptimisticState, input: ActionInput<TAction>) => TOptimisticState;\n preserveOnError?: boolean;\n};\n\nexport function useOptimisticAction<TAction extends AnyAction, TOptimisticState>(\n action: TAction,\n options: UseOptimisticActionOptions<TAction, TOptimisticState>,\n) {\n const [optimisticState, setOptimisticState] = useState(options.initialState);\n const optimisticStateRef = useRef(options.initialState);\n const rollbackStateRef = useRef<TOptimisticState | undefined>(undefined);\n const [isPendingTransition, startTransition] = useTransition();\n\n const actionState = useAction(action, {\n ...options,\n onSuccess: (data) => {\n rollbackStateRef.current = undefined;\n options.onSuccess?.(data as ActionData<TAction>);\n },\n onValidationError: (validationErrors) => {\n if (!options.preserveOnError && rollbackStateRef.current !== undefined) {\n optimisticStateRef.current = rollbackStateRef.current;\n setOptimisticState(rollbackStateRef.current);\n }\n rollbackStateRef.current = undefined;\n options.onValidationError?.(validationErrors);\n },\n onServerError: (serverError) => {\n if (!options.preserveOnError && rollbackStateRef.current !== undefined) {\n optimisticStateRef.current = rollbackStateRef.current;\n setOptimisticState(rollbackStateRef.current);\n }\n rollbackStateRef.current = undefined;\n options.onServerError?.(serverError);\n },\n onSettled: (result) => {\n options.onSettled?.(result as ActionResult<TAction>);\n },\n });\n\n const executeAsync = useCallback(\n async (...args: Parameters<TAction>): Promise<ActionResult<TAction>> => {\n const input = args[0] as ActionInput<TAction>;\n rollbackStateRef.current = optimisticStateRef.current;\n\n const nextOptimisticState = options.updateFn(optimisticStateRef.current, input);\n optimisticStateRef.current = nextOptimisticState;\n setOptimisticState(nextOptimisticState);\n\n return (await actionState.executeAsync(...args)) as ActionResult<TAction>;\n },\n [actionState, options],\n );\n\n const execute = useCallback(\n (...args: Parameters<TAction>) => {\n startTransition(() => {\n void executeAsync(...args);\n });\n },\n [executeAsync, startTransition],\n );\n\n const reset = useCallback(() => {\n optimisticStateRef.current = options.initialState;\n rollbackStateRef.current = undefined;\n setOptimisticState(options.initialState);\n actionState.reset();\n }, [actionState, options.initialState]);\n\n return {\n execute,\n executeAsync,\n result: actionState.result,\n status: actionState.status,\n isPending: actionState.isPending || isPendingTransition,\n hasExecuted: actionState.hasExecuted,\n reset,\n optimisticState,\n };\n}\n","import { useCallback, useState } from 'react';\n\nimport type { SafeActionResult, SafeActionValidationErrors } from '../safeActionTypes';\nimport { useAction } from './useAction';\n\ntype AnyAction = (...args: never[]) => Promise<SafeActionResult<unknown>>;\n\ntype ActionResult<TAction extends AnyAction> = Awaited<ReturnType<TAction>>;\n\ntype ActionInput<TAction extends AnyAction> = Parameters<TAction> extends [] ? void : Parameters<TAction>[0];\n\ntype ActionData<TAction extends AnyAction> =\n ActionResult<TAction> extends SafeActionResult<infer TData> ? TData : never;\n\nexport type UseStateActionOptions<TAction extends AnyAction, TState> = {\n initialState: TState;\n mapFormData?: (formData: FormData) => ActionInput<TAction>;\n onSuccess?: (prevState: TState, data: ActionData<TAction>) => TState;\n onValidationError?: (prevState: TState, validationErrors: SafeActionValidationErrors) => TState;\n onServerError?: (prevState: TState, serverError: string) => TState;\n onSettled?: (result: ActionResult<TAction>) => void;\n};\n\nexport function useStateAction<TAction extends AnyAction, TState>(\n action: TAction,\n options: UseStateActionOptions<TAction, TState>,\n) {\n const [state, setState] = useState(options.initialState);\n\n const actionState = useAction(action, {\n onSuccess: (data) => {\n if (options.onSuccess) {\n setState((prevState) => options.onSuccess!(prevState, data as ActionData<TAction>));\n }\n },\n onValidationError: (validationErrors) => {\n if (options.onValidationError) {\n setState((prevState) => options.onValidationError!(prevState, validationErrors));\n }\n },\n onServerError: (serverError) => {\n if (options.onServerError) {\n setState((prevState) => options.onServerError!(prevState, serverError));\n }\n },\n onSettled: (result) => {\n options.onSettled?.(result as ActionResult<TAction>);\n },\n });\n\n const formAction = useCallback(\n async (formData: FormData) => {\n const mapper =\n options.mapFormData ??\n ((value: FormData) => Object.fromEntries(value.entries()) as unknown as ActionInput<TAction>);\n const mappedInput = mapper(formData);\n return (actionState.executeAsync as (...args: unknown[]) => Promise<ActionResult<TAction>>)(mappedInput);\n },\n [actionState, options],\n );\n\n const reset = useCallback(() => {\n setState(options.initialState);\n actionState.reset();\n }, [actionState, options.initialState]);\n\n return {\n formAction,\n execute: actionState.execute,\n executeAsync: actionState.executeAsync,\n result: actionState.result,\n status: actionState.status,\n isPending: actionState.isPending,\n hasExecuted: actionState.hasExecuted,\n reset,\n state,\n };\n}\n"],"mappings":"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","names":["useCallback","useState","useTransition","getStatusFromResult","result","useAction","action","options","setResult","status","setStatus","hasExecuted","setHasExecuted","isPending","startTransition","executeAsync","args","actionResult","nextStatus","validationErrors","execute","reset","useCallback","useRef","useState","useTransition","useOptimisticAction","action","options","optimisticState","setOptimisticState","useState","optimisticStateRef","useRef","rollbackStateRef","isPendingTransition","startTransition","useTransition","actionState","useAction","data","validationErrors","serverError","result","executeAsync","useCallback","args","input","nextOptimisticState","execute","reset","useCallback","useState","useStateAction","action","options","state","setState","useState","actionState","useAction","data","prevState","validationErrors","serverError","result","formAction","useCallback","formData","mappedInput","value","reset"]}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,6 @@
1
- import { S as Schema, I as Infer, V as ValidationIssue, a as ValidationAdapter, b as IfInstalled } from './types-UXG9BoMB.mjs';
1
+ import { S as Schema, a as Infer, b as ValidationIssue, V as ValidationAdapter, c as IfInstalled } from './types-DYbZEItT.mjs';
2
+ import { b as SafeActionBuilderConfig, c as SafeActionMiddleware, I as InferParsedActionInput, d as SafeActionHandler, e as SafeActionFn, f as InferActionInput, g as SafeActionClientOptions } from './safeActionTypes-DfvihJur.mjs';
3
+ export { h as SafeActionMiddlewareNext, a as SafeActionResult, i as SafeActionServerErrorResult, j as SafeActionSuccessResult, k as SafeActionValidationErrorResult, S as SafeActionValidationErrors } from './safeActionTypes-DfvihJur.mjs';
2
4
  import { z } from 'zod';
3
5
  export { valibotAdapter } from './valibot.mjs';
4
6
  export { yupAdapter } from './yup.mjs';
@@ -6,10 +8,34 @@ import '@sinclair/typebox';
6
8
  import 'valibot';
7
9
  import 'yup';
8
10
 
9
- type Awaitable<T> = T | Promise<T>;
11
+ type Awaitable$1<T> = T | Promise<T>;
12
+ type ValueCoercionMode = 'none' | 'primitive';
13
+ type ValueCoercionFn = (value: string, key: string) => unknown;
14
+ type ValueCoercion = ValueCoercionMode | ValueCoercionFn;
15
+ type QueryArrayStrategy = 'auto' | 'always' | 'never';
16
+ type QuerySingleValueStrategy = 'first' | 'last';
17
+ type QueryParserOptions = {
18
+ arrayStrategy?: QueryArrayStrategy;
19
+ singleValueStrategy?: QuerySingleValueStrategy;
20
+ coerce?: ValueCoercion;
21
+ };
22
+ type BodyFallbackStrategy = 'json-first' | 'text';
23
+ type BodyParserOptions = {
24
+ strictContentType?: boolean;
25
+ allowEmptyBody?: boolean;
26
+ emptyValue?: unknown;
27
+ coerce?: ValueCoercion;
28
+ fallbackStrategy?: BodyFallbackStrategy;
29
+ arrayStrategy?: QueryArrayStrategy;
30
+ singleValueStrategy?: QuerySingleValueStrategy;
31
+ };
32
+ type ParserOptions = {
33
+ query?: QueryParserOptions;
34
+ body?: BodyParserOptions;
35
+ };
10
36
  type InferMaybe<TSchema extends Schema | undefined> = TSchema extends Schema ? Infer<TSchema> : Record<string, unknown>;
11
37
  type RouteContext<TRawParams extends Record<string, unknown> = Record<string, string | string[]>> = {
12
- params: Awaitable<TRawParams>;
38
+ params: Awaitable$1<TRawParams>;
13
39
  };
14
40
  type HandlerFunction<TParams, TQuery, TBody, TContext> = (request: Request, context: {
15
41
  params: TParams;
@@ -21,7 +47,9 @@ type OriginalRouteHandler = (request: Request, context: RouteContext) => Respons
21
47
  type HandlerServerErrorFn = (error: Error) => Response;
22
48
  type ValidationErrorHandler = (issues: ValidationIssue[]) => Response;
23
49
 
24
- type Middleware<T = Record<string, unknown>> = (request: Request) => Promise<T | Response>;
50
+ type Awaitable<T> = T | Promise<T>;
51
+ type Middleware<TContext extends Record<string, unknown> = Record<string, unknown>, TReturn extends Record<string, unknown> = Record<string, unknown>> = (request: Request, data: TContext) => Awaitable<TReturn | Response>;
52
+ type AnyMiddleware = Middleware<Record<string, unknown>, Record<string, unknown>>;
25
53
  type BuilderConfig<TParams extends Schema | undefined, TQuery extends Schema | undefined, TBody extends Schema | undefined> = {
26
54
  paramsSchema: TParams;
27
55
  querySchema: TQuery;
@@ -34,22 +62,31 @@ declare class RouteHandlerBuilder<TParams extends Schema | undefined = undefined
34
62
  private validationErrorHandler?;
35
63
  private validationAdapter;
36
64
  private baseContext;
37
- constructor({ config, validationAdapter, middlewares, handleServerError, validationErrorHandler, baseContext, }: {
65
+ private parserOptions;
66
+ private parserOptionsInput?;
67
+ constructor({ config, validationAdapter, middlewares, handleServerError, validationErrorHandler, baseContext, parserOptions, }: {
38
68
  config?: BuilderConfig<TParams, TQuery, TBody>;
39
- middlewares?: Middleware[];
69
+ middlewares?: AnyMiddleware[];
40
70
  handleServerError?: HandlerServerErrorFn;
41
71
  validationErrorHandler?: ValidationErrorHandler;
42
72
  validationAdapter?: ValidationAdapter;
43
73
  baseContext?: TContext;
74
+ parserOptions?: ParserOptions;
44
75
  });
45
76
  params<T extends Schema>(schema: T): RouteHandlerBuilder<T, TQuery, TBody, TContext>;
46
77
  query<T extends Schema>(schema: T): RouteHandlerBuilder<TParams, T, TBody, TContext>;
47
78
  body<T extends Schema>(schema: T): RouteHandlerBuilder<TParams, TQuery, T, TContext>;
48
- use<TReturnType extends Record<string, unknown>>(middleware: Middleware<TReturnType>): RouteHandlerBuilder<TParams, TQuery, TBody, TContext & TReturnType>;
79
+ use<TReturnType extends Record<string, unknown>>(middleware: Middleware<TContext, TReturnType>): RouteHandlerBuilder<TParams, TQuery, TBody, TContext & TReturnType>;
49
80
  handler(handler: HandlerFunction<InferMaybe<TParams>, InferMaybe<TQuery>, InferMaybe<TBody>, TContext>): OriginalRouteHandler;
50
81
  private resolveParams;
51
82
  private validateInput;
52
83
  private getQueryParams;
84
+ private selectValues;
85
+ private pickSingleValue;
86
+ private coerceStringValue;
87
+ private coercePrimitiveValue;
88
+ private parseFormData;
89
+ private resolveEmptyBody;
53
90
  private parseRequestBody;
54
91
  private buildErrorResponse;
55
92
  }
@@ -59,9 +96,23 @@ type CreateSafeRouteParams<TContext extends Record<string, unknown>> = {
59
96
  validationErrorHandler?: ValidationErrorHandler;
60
97
  validationAdapter?: ValidationAdapter;
61
98
  baseContext?: TContext;
99
+ parserOptions?: ParserOptions;
62
100
  };
63
101
  declare function createSafeRoute<TContext extends Record<string, unknown> = Record<string, unknown>>(params?: CreateSafeRouteParams<TContext>): RouteHandlerBuilder<undefined, undefined, undefined, TContext>;
64
102
 
103
+ type DefaultObject = Record<string, never>;
104
+ declare class SafeActionBuilder<TInputSchema extends Schema | undefined = undefined, TOutputSchema extends Schema | undefined = undefined, TContext extends Record<string, unknown> = Record<string, unknown>, TMetadata extends Record<string, unknown> = DefaultObject> {
105
+ private config;
106
+ constructor(config: SafeActionBuilderConfig<TInputSchema, TOutputSchema, TContext, TMetadata>);
107
+ inputSchema<TSchema extends Schema>(schema: TSchema): SafeActionBuilder<TSchema, TOutputSchema, TContext, TMetadata>;
108
+ outputSchema<TSchema extends Schema>(schema: TSchema): SafeActionBuilder<TInputSchema, TSchema, TContext, TMetadata>;
109
+ metadata<TNextMetadata extends Record<string, unknown>>(metadata: TNextMetadata): SafeActionBuilder<TInputSchema, TOutputSchema, TContext, TNextMetadata>;
110
+ use<TContextPatch extends Record<string, unknown>>(middleware: SafeActionMiddleware<InferParsedActionInput<TInputSchema>, TContext, TMetadata, TContextPatch>): SafeActionBuilder<TInputSchema, TOutputSchema, TContext & TContextPatch, TMetadata>;
111
+ action<TData>(handler: SafeActionHandler<InferParsedActionInput<TInputSchema>, TContext, TMetadata, TData>): SafeActionFn<InferActionInput<TInputSchema>, TOutputSchema extends Schema ? Infer<TOutputSchema> : TData>;
112
+ }
113
+
114
+ declare function createSafeActionClient<TContext extends Record<string, unknown> = Record<string, unknown>>(options?: SafeActionClientOptions<TContext>): SafeActionBuilder<undefined, undefined, TContext, Record<string, never>>;
115
+
65
116
  declare class ZodAdapter implements ValidationAdapter {
66
117
  validate<S extends IfInstalled<z.ZodTypeAny>>(schema: S, data: unknown): Promise<{
67
118
  readonly success: true;
@@ -78,4 +129,4 @@ declare class ZodAdapter implements ValidationAdapter {
78
129
  }
79
130
  declare function zodAdapter(): ZodAdapter;
80
131
 
81
- export { type HandlerFunction, type HandlerServerErrorFn, IfInstalled, Infer, type InferMaybe, type OriginalRouteHandler, type RouteContext, RouteHandlerBuilder, Schema, ValidationAdapter, ValidationIssue, createSafeRoute, zodAdapter };
132
+ export { type BodyFallbackStrategy, type BodyParserOptions, type HandlerFunction, type HandlerServerErrorFn, IfInstalled, Infer, InferActionInput, type InferMaybe, InferParsedActionInput, type OriginalRouteHandler, type ParserOptions, type QueryArrayStrategy, type QueryParserOptions, type QuerySingleValueStrategy, type RouteContext, RouteHandlerBuilder, SafeActionBuilder, SafeActionBuilderConfig, SafeActionClientOptions, SafeActionFn, SafeActionHandler, SafeActionMiddleware, Schema, ValidationAdapter, ValidationIssue, type ValueCoercion, type ValueCoercionFn, type ValueCoercionMode, createSafeActionClient, createSafeRoute, zodAdapter };