Skip to content

Commit c9d02e0

Browse files
authored
feat: add action throwValidationErrors and throwServerError util props (#208)
Code in this PR adds `throwValidationErrors` and `throwServerError` optional properties at the action level.
1 parent 84f94fb commit c9d02e0

File tree

9 files changed

+174
-31
lines changed

9 files changed

+174
-31
lines changed

packages/next-safe-action/src/__tests__/server-error.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ test("known error occurred in middleware function is unmasked", async () => {
9393
assert.deepStrictEqual(actualResult, expectedResult);
9494
});
9595

96+
test("error occurred with `throwServerError` set to true at the action level throws", async () => {
97+
const action = ac1.action(
98+
async () => {
99+
throw new Error("Something bad happened");
100+
},
101+
{ throwServerError: true }
102+
);
103+
104+
assert.rejects(async () => await action());
105+
});
106+
96107
// Server error is an object with a 'message' property.
97108
const ac2 = createSafeActionClient({
98109
validationAdapter: zodAdapter(),

packages/next-safe-action/src/__tests__/validation-errors.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,25 @@ test("action with errors set via `returnValidationErrors` gives back an object w
541541

542542
// `throwValidationErrors` tests.
543543

544+
// test without `throwValidationErrors` set at the instance level, just set at the action level.
545+
test("action with validation errors and `throwValidationErrors` option set to true at the action level throws", async () => {
546+
const schema = z.object({
547+
username: z.string().min(3),
548+
password: z.string().min(3),
549+
});
550+
551+
const action = dac.schema(schema).action(
552+
async () => {
553+
return {
554+
ok: true,
555+
};
556+
},
557+
{ throwValidationErrors: true }
558+
);
559+
560+
assert.rejects(async () => await action({ username: "12", password: "34" }));
561+
});
562+
544563
const tveac = createSafeActionClient({
545564
validationAdapter: zodAdapter(),
546565
throwValidationErrors: true,
@@ -580,3 +599,78 @@ test("action with server validation errors and `throwValidationErrors` option se
580599

581600
assert.rejects(async () => await action({ username: "1234", password: "5678" }));
582601
});
602+
603+
test("action with validation errors and `throwValidationErrors` option set to true both in client and action throws", async () => {
604+
const schema = z.object({
605+
username: z.string().min(3),
606+
password: z.string().min(3),
607+
});
608+
609+
const action = tveac.schema(schema).action(
610+
async () => {
611+
return {
612+
ok: true,
613+
};
614+
},
615+
{ throwValidationErrors: true }
616+
);
617+
618+
assert.rejects(async () => await action({ username: "12", password: "34" }));
619+
});
620+
621+
test("action with validation errors and overridden `throwValidationErrors` set to false at the action level doesn't throw", async () => {
622+
const schema = z.object({
623+
user: z.object({
624+
id: z.string().min(36).uuid(),
625+
}),
626+
store: z.object({
627+
id: z.string().min(36).uuid(),
628+
product: z.object({
629+
id: z.string().min(36).uuid(),
630+
}),
631+
}),
632+
});
633+
634+
const action = tveac.schema(schema).action(
635+
async () => {
636+
return {
637+
ok: true,
638+
};
639+
},
640+
{ throwValidationErrors: false }
641+
);
642+
643+
const actualResult = await action({
644+
user: {
645+
id: "invalid_uuid",
646+
},
647+
store: {
648+
id: "invalid_uuid",
649+
product: {
650+
id: "invalid_uuid",
651+
},
652+
},
653+
});
654+
655+
const expectedResult = {
656+
validationErrors: {
657+
user: {
658+
id: {
659+
_errors: ["String must contain at least 36 character(s)", "Invalid uuid"],
660+
},
661+
},
662+
store: {
663+
id: {
664+
_errors: ["String must contain at least 36 character(s)", "Invalid uuid"],
665+
},
666+
product: {
667+
id: {
668+
_errors: ["String must contain at least 36 character(s)", "Invalid uuid"],
669+
},
670+
},
671+
},
672+
},
673+
};
674+
675+
assert.deepStrictEqual(actualResult, expectedResult);
676+
});

packages/next-safe-action/src/action-builder.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import type { Infer, InferArray, InferIn, InferInArray, Schema, ValidationAdapte
55
import type {
66
MiddlewareFn,
77
MiddlewareResult,
8-
SafeActionCallbacks,
98
SafeActionClientOpts,
109
SafeActionFn,
1110
SafeActionResult,
11+
SafeActionUtils,
1212
SafeStateActionFn,
1313
ServerCodeFn,
1414
StateServerCodeFn,
@@ -53,13 +53,13 @@ export function actionBuilder<
5353
function buildAction({ withState }: { withState: false }): {
5454
action: <Data>(
5555
serverCodeFn: ServerCodeFn<MD, Ctx, S, BAS, Data>,
56-
cb?: SafeActionCallbacks<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>
56+
utils?: SafeActionUtils<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>
5757
) => SafeActionFn<ServerError, S, BAS, CVE, CBAVE, Data>;
5858
};
5959
function buildAction({ withState }: { withState: true }): {
6060
action: <Data>(
6161
serverCodeFn: StateServerCodeFn<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>,
62-
cb?: SafeActionCallbacks<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>
62+
utils?: SafeActionUtils<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>
6363
) => SafeStateActionFn<ServerError, S, BAS, CVE, CBAVE, Data>;
6464
};
6565
function buildAction({ withState }: { withState: boolean }) {
@@ -68,7 +68,7 @@ export function actionBuilder<
6868
serverCodeFn:
6969
| ServerCodeFn<MD, Ctx, S, BAS, Data>
7070
| StateServerCodeFn<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>,
71-
cb?: SafeActionCallbacks<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>
71+
utils?: SafeActionUtils<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>
7272
) => {
7373
return async (...clientInputs: unknown[]) => {
7474
let prevCtx: unknown = undefined;
@@ -260,7 +260,7 @@ export function actionBuilder<
260260
// If an internal framework error occurred, throw it, so it will be processed by Next.js.
261261
if (frameworkError) {
262262
await Promise.resolve(
263-
cb?.onSuccess?.({
263+
utils?.onSuccess?.({
264264
data: undefined,
265265
metadata: args.metadata,
266266
ctx: prevCtx as Ctx,
@@ -274,7 +274,7 @@ export function actionBuilder<
274274
);
275275

276276
await Promise.resolve(
277-
cb?.onSettled?.({
277+
utils?.onSettled?.({
278278
metadata: args.metadata,
279279
ctx: prevCtx as Ctx,
280280
clientInput: clientInputs.at(-1) as S extends Schema ? InferIn<S> : undefined,
@@ -291,18 +291,29 @@ export function actionBuilder<
291291
const actionResult: SafeActionResult<ServerError, S, BAS, CVE, CBAVE, Data> = {};
292292

293293
if (typeof middlewareResult.validationErrors !== "undefined") {
294-
if (args.throwValidationErrors) {
294+
// Throw validation errors if either `throwValidationErrors` property at the action or instance level is `true`.
295+
// If `throwValidationErrors` property at the action is `false`, do not throw validation errors, since it
296+
// has a higher priority than the instance one.
297+
if (
298+
(utils?.throwValidationErrors || args.throwValidationErrors) &&
299+
utils?.throwValidationErrors !== false
300+
) {
295301
throw new ActionValidationError(middlewareResult.validationErrors as CVE);
302+
} else {
303+
actionResult.validationErrors = middlewareResult.validationErrors as CVE;
296304
}
297-
actionResult.validationErrors = middlewareResult.validationErrors as CVE;
298305
}
299306

300307
if (typeof middlewareResult.bindArgsValidationErrors !== "undefined") {
301308
actionResult.bindArgsValidationErrors = middlewareResult.bindArgsValidationErrors as CBAVE;
302309
}
303310

304311
if (typeof middlewareResult.serverError !== "undefined") {
305-
actionResult.serverError = middlewareResult.serverError;
312+
if (utils?.throwServerError) {
313+
throw middlewareResult.serverError;
314+
} else {
315+
actionResult.serverError = middlewareResult.serverError;
316+
}
306317
}
307318

308319
if (middlewareResult.success) {
@@ -311,7 +322,7 @@ export function actionBuilder<
311322
}
312323

313324
await Promise.resolve(
314-
cb?.onSuccess?.({
325+
utils?.onSuccess?.({
315326
metadata: args.metadata,
316327
ctx: prevCtx as Ctx,
317328
data: actionResult.data as Data,
@@ -325,7 +336,7 @@ export function actionBuilder<
325336
);
326337
} else {
327338
await Promise.resolve(
328-
cb?.onError?.({
339+
utils?.onError?.({
329340
metadata: args.metadata,
330341
ctx: prevCtx as Ctx,
331342
clientInput: clientInputs.at(-1) as S extends Schema ? InferIn<S> : undefined,
@@ -337,7 +348,7 @@ export function actionBuilder<
337348

338349
// onSettled, if provided, is always executed.
339350
await Promise.resolve(
340-
cb?.onSettled?.({
351+
utils?.onSettled?.({
341352
metadata: args.metadata,
342353
ctx: prevCtx as Ctx,
343354
clientInput: clientInputs.at(-1) as S extends Schema ? InferIn<S> : undefined,

packages/next-safe-action/src/index.types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ export type StateServerCodeFn<
145145
) => Promise<Data>;
146146

147147
/**
148-
* Type of action execution callbacks. These are called after the action is executed, on the server side.
148+
* Type of action execution utils. It includes action callbacks and other utils.
149149
*/
150-
export type SafeActionCallbacks<
150+
export type SafeActionUtils<
151151
ServerError,
152152
MD,
153153
Ctx,
@@ -157,6 +157,8 @@ export type SafeActionCallbacks<
157157
CBAVE,
158158
Data,
159159
> = {
160+
throwServerError?: boolean;
161+
throwValidationErrors?: boolean;
160162
onSuccess?: (args: {
161163
data?: Data;
162164
metadata: MD;

packages/next-safe-action/src/safe-action-client.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import type { Infer, Schema, ValidationAdapter } from "./adapters/types";
44
import type {
55
DVES,
66
MiddlewareFn,
7-
SafeActionCallbacks,
87
SafeActionClientOpts,
8+
SafeActionUtils,
99
ServerCodeFn,
1010
StateServerCodeFn,
1111
} from "./index.types";
@@ -213,7 +213,7 @@ export class SafeActionClient<
213213
*/
214214
action<Data>(
215215
serverCodeFn: ServerCodeFn<MD, Ctx, S, BAS, Data>,
216-
cb?: SafeActionCallbacks<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>
216+
utils?: SafeActionUtils<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>
217217
) {
218218
return actionBuilder({
219219
handleReturnedServerError: this.#handleReturnedServerError,
@@ -228,7 +228,7 @@ export class SafeActionClient<
228228
handleValidationErrorsShape: this.#handleValidationErrorsShape,
229229
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
230230
throwValidationErrors: this.#throwValidationErrors,
231-
}).action(serverCodeFn, cb);
231+
}).action(serverCodeFn, utils);
232232
}
233233

234234
/**
@@ -241,7 +241,7 @@ export class SafeActionClient<
241241
*/
242242
stateAction<Data>(
243243
serverCodeFn: StateServerCodeFn<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>,
244-
cb?: SafeActionCallbacks<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>
244+
utils?: SafeActionUtils<ServerError, MD, Ctx, S, BAS, CVE, CBAVE, Data>
245245
) {
246246
return actionBuilder({
247247
handleReturnedServerError: this.#handleReturnedServerError,
@@ -256,6 +256,6 @@ export class SafeActionClient<
256256
handleValidationErrorsShape: this.#handleValidationErrorsShape,
257257
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
258258
throwValidationErrors: this.#throwValidationErrors,
259-
}).stateAction(serverCodeFn, cb);
259+
}).stateAction(serverCodeFn, utils);
260260
}
261261
}

website/docs/execution/action-callbacks.md renamed to website/docs/execution/action-utils.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
---
22
sidebar_position: 3
3-
description: Action callbacks are a way to perform custom logic after the action is executed, on the server.
3+
description: Action utils is an object with useful properties and callbacks functions that you can use to customize the action execution flow.
44
---
55

6-
# Action callbacks
6+
# Action utils
77

8-
With action callbacks you can perform custom logic after the action is executed, on the server side. You can provide them to [`action`/`stateAction`](/docs/safe-action-client/instance-methods#action--stateaction) method as the second argument, after the server code function:
8+
Action utils is an object with some useful properties and callbacks passed as the second argument of the [`action`/`stateAction`](/docs/safe-action-client/instance-methods#action--stateaction) method.
9+
10+
## Throw errors when they occur
11+
12+
Starting from v7.4.0, you can now pass optional `throwServerError` and `throwValidationErrors` properties at the action level, if you want or need that behavior. Note that the `throwValidationErrors` property set at the action level has a higher priority than the one at the instance level, so if you set it to `false` while the one at the instance level is `true`, validation errors will **not** be thrown.
13+
14+
15+
## Callbacks
16+
17+
With action callbacks you can perform custom logic after the action is executed, on the server side. You can provide them to [`action`/`stateAction`](/docs/safe-action-client/instance-methods#action--stateaction) method in the second argument, after the server code function:
918

1019
```tsx
1120
import { actionClient } from "@/lib/safe-action";
@@ -27,8 +36,22 @@ const action = actionClient
2736
hasRedirected,
2837
hasNotFound,
2938
}) => {},
30-
onError: ({ error, ctx, metadata, clientInput, bindArgsClientInputs }) => {},
31-
onSettled: ({ result, ctx, metadata, clientInput, bindArgsClientInputs }) => {},
39+
onError: ({
40+
error,
41+
ctx,
42+
metadata,
43+
clientInput,
44+
bindArgsClientInputs
45+
}) => {},
46+
onSettled: ({
47+
result,
48+
ctx,
49+
metadata,
50+
clientInput,
51+
bindArgsClientInputs,
52+
hasRedirected,
53+
hasNotFound
54+
}) => {},
3255
});
3356
```
3457

website/docs/migrations/v6-to-v7.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ When working with i18n solutions, often you'll find implementations that require
149149

150150
### [Support action execution callbacks](https://github.com/TheEdoRan/next-safe-action/issues/162)
151151

152-
It's sometimes useful to be able to execute custom logic on the server side after an action succeeds or fails. Starting from version 7, next-safe-action allows you to pass action callbacks when defining an action. More information about this feature can be found [here](/docs/execution/action-callbacks).
152+
It's sometimes useful to be able to execute custom logic on the server side after an action succeeds or fails. Starting from version 7, next-safe-action allows you to pass action callbacks when defining an action. More information about this feature can be found [here](/docs/execution/action-utils#callbacks).
153153

154154
### [Support stateful actions using React `useActionState` hook](https://github.com/TheEdoRan/next-safe-action/issues/91)
155155

website/docs/safe-action-client/instance-methods.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ bindArgsSchemas(bindArgsSchemas: BAS, bindArgsUtils?: { handleBindArgsValidation
4444
## `action` / `stateAction`
4545

4646
```typescript
47-
action(serverCodeFn: ServerCodeFn, cb?: SafeActionCallbacks) => SafeActionFn
47+
action(serverCodeFn: ServerCodeFn, utils?: SafeActionUtils) => SafeActionFn
4848
```
4949

5050
```typescript
51-
stateAction(serverCodeFn: StateServerCodeFn, cb?: SafeActionCallbacks) => SafeStateActionFn
51+
stateAction(serverCodeFn: StateServerCodeFn, utils?: SafeActionUtils) => SafeStateActionFn
5252
```
5353

54-
`action`/`stateAction` is the final method in the list. It accepts a [`serverCodeFn`](#servercodefn) of type [`ServerCodeFn`](/docs/types#servercodefn)/[`StateServerCodeFn`](/docs/types#stateservercodefn) and an object with optional [action callbacks](/docs/execution/action-callbacks), and it returns a new safe action function of type [`SafeActionFn`](/docs/types#safeactionfn)/[`SafeStateActionFn`](/docs/types#safestateactionfn), which can be called from your components. When an action doesn't need input arguments, you can directly use this method without passing a schema to [`schema`](#schema) method.
54+
`action`/`stateAction` is the final method in the list. It accepts a [`serverCodeFn`](#servercodefn) of type [`ServerCodeFn`](/docs/types#servercodefn)/[`StateServerCodeFn`](/docs/types#stateservercodefn) and an optional object with [action utils](/docs/execution/action-utils), and it returns a new safe action function of type [`SafeActionFn`](/docs/types#safeactionfn)/[`SafeStateActionFn`](/docs/types#safestateactionfn), which can be called from your components. When an action doesn't need input arguments, you can directly use this method without passing a schema to [`schema`](#schema) method.
5555

5656
When the action is executed, all middleware functions in the chain will be called at runtime, in the order they were defined.
5757

0 commit comments

Comments
 (0)