Skip to content

Commit 66d4ea3

Browse files
authoredApr 16, 2024··
feat(validation-errors): support customization of validation errors format (#101)
This PR adds the ability to return a custom validation errors format (both main argument validation errors and bind arguments validation errors) to the client, via `formatValidationErrors` in `schema` method and `formatBindArgsValidationErrors` in `bindArgsSchemas` method. re #98
1 parent 218176d commit 66d4ea3

21 files changed

+537
-148
lines changed
 

‎package-lock.json

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/example-app/src/app/(examples)/bind-arguments/onboard-action.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ const schema = z.object({
77
username: z.string().min(3).max(30),
88
});
99

10+
const bindArgsSchemas: [userId: z.ZodString, age: z.ZodNumber] = [
11+
z.string().uuid(),
12+
z.number().min(18).max(150),
13+
];
14+
1015
export const onboardUser = action
1116
.metadata({ actionName: "onboardUser" })
1217
.schema(schema)
13-
.bindArgsSchemas<[userId: z.ZodString, age: z.ZodNumber]>([
14-
z.string().uuid(),
15-
z.number().min(18).max(150),
16-
])
18+
.bindArgsSchemas(bindArgsSchemas)
1719
.action(
1820
async ({
1921
parsedInput: { username },

‎packages/example-app/src/app/(examples)/direct/login-action.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"use server";
22

33
import { action } from "@/lib/safe-action";
4-
import { returnValidationErrors } from "next-safe-action";
4+
import {
5+
flattenValidationErrors,
6+
returnValidationErrors,
7+
} from "next-safe-action";
58
import { z } from "zod";
69

710
const schema = z.object({
@@ -11,7 +14,11 @@ const schema = z.object({
1114

1215
export const loginUser = action
1316
.metadata({ actionName: "loginUser" })
14-
.schema(schema)
17+
.schema(schema, {
18+
// Here we use the `flattenValidationErrors` function to customize the returned validation errors
19+
// object to the client.
20+
formatValidationErrors: (ve) => flattenValidationErrors(ve).fieldErrors,
21+
})
1522
.action(async ({ parsedInput: { username, password } }) => {
1623
if (username === "johndoe") {
1724
returnValidationErrors(schema, {

‎packages/example-app/src/lib/safe-action.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export const action = createSafeActionClient({
4343
logObject.metadata = metadata;
4444
logObject.result = result;
4545

46-
console.log("MIDDLEWARE LOG:", logObject);
46+
console.log("LOGGING FROM MIDDLEWARE:");
47+
console.dir(logObject, { depth: null });
4748

4849
// And then return the result of the awaited next middleware.
4950
return result;

‎packages/next-safe-action/src/hooks.ts

+26-16
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ const DEFAULT_RESULT = {
1515
fetchError: undefined,
1616
serverError: undefined,
1717
validationErrors: undefined,
18-
} satisfies HookResult<any, any, any, any>;
18+
} satisfies HookResult<any, any, any, any, any, any>;
1919

2020
const getActionStatus = <
2121
const ServerError,
2222
const S extends Schema,
23-
const BAS extends Schema[],
23+
const BAS extends readonly Schema[],
24+
const FVE,
25+
const FBAVE,
2426
const Data,
2527
>({
2628
isExecuting,
2729
result,
2830
}: {
2931
isExecuting: boolean;
30-
result: HookResult<ServerError, S, BAS, Data>;
32+
result: HookResult<ServerError, S, BAS, FVE, FBAVE, Data>;
3133
}): HookActionStatus => {
3234
if (isExecuting) {
3335
return "executing";
@@ -48,7 +50,9 @@ const getActionStatus = <
4850
const useActionCallbacks = <
4951
const ServerError,
5052
const S extends Schema,
51-
const BAS extends Schema[],
53+
const BAS extends readonly Schema[],
54+
const FVE,
55+
const FBAVE,
5256
const Data,
5357
>({
5458
result,
@@ -57,11 +61,11 @@ const useActionCallbacks = <
5761
reset,
5862
cb,
5963
}: {
60-
result: HookResult<ServerError, S, BAS, Data>;
64+
result: HookResult<ServerError, S, BAS, FVE, FBAVE, Data>;
6165
input: InferIn<S>;
6266
status: HookActionStatus;
6367
reset: () => void;
64-
cb?: HookCallbacks<ServerError, S, BAS, Data>;
68+
cb?: HookCallbacks<ServerError, S, BAS, FVE, FBAVE, Data>;
6569
}) => {
6670
const onExecuteRef = React.useRef(cb?.onExecute);
6771
const onSuccessRef = React.useRef(cb?.onSuccess);
@@ -107,18 +111,21 @@ const useActionCallbacks = <
107111
export const useAction = <
108112
const ServerError,
109113
const S extends Schema,
110-
const BAS extends Schema[],
114+
const BAS extends readonly Schema[],
115+
const FVE,
116+
const FBAVE,
111117
const Data,
112118
>(
113-
safeActionFn: HookSafeActionFn<ServerError, S, BAS, Data>,
114-
callbacks?: HookCallbacks<ServerError, S, BAS, Data>
119+
safeActionFn: HookSafeActionFn<ServerError, S, BAS, FVE, FBAVE, Data>,
120+
callbacks?: HookCallbacks<ServerError, S, BAS, FVE, FBAVE, Data>
115121
) => {
116122
const [, startTransition] = React.useTransition();
117-
const [result, setResult] = React.useState<HookResult<ServerError, S, BAS, Data>>(DEFAULT_RESULT);
123+
const [result, setResult] =
124+
React.useState<HookResult<ServerError, S, BAS, FVE, FBAVE, Data>>(DEFAULT_RESULT);
118125
const [input, setInput] = React.useState<InferIn<S>>();
119126
const [isExecuting, setIsExecuting] = React.useState(false);
120127

121-
const status = getActionStatus<ServerError, S, BAS, Data>({ isExecuting, result });
128+
const status = getActionStatus<ServerError, S, BAS, FVE, FBAVE, Data>({ isExecuting, result });
122129

123130
const execute = React.useCallback(
124131
(input: InferIn<S>) => {
@@ -171,16 +178,19 @@ export const useAction = <
171178
export const useOptimisticAction = <
172179
const ServerError,
173180
const S extends Schema,
174-
const BAS extends Schema[],
181+
const BAS extends readonly Schema[],
182+
const FVE,
183+
const FBAVE,
175184
const Data,
176185
>(
177-
safeActionFn: HookSafeActionFn<ServerError, S, BAS, Data>,
186+
safeActionFn: HookSafeActionFn<ServerError, S, BAS, FVE, FBAVE, Data>,
178187
initialOptimisticData: Data,
179188
reducer: (state: Data, input: InferIn<S>) => Data,
180-
callbacks?: HookCallbacks<ServerError, S, BAS, Data>
189+
callbacks?: HookCallbacks<ServerError, S, BAS, FVE, FBAVE, Data>
181190
) => {
182191
const [, startTransition] = React.useTransition();
183-
const [result, setResult] = React.useState<HookResult<ServerError, S, BAS, Data>>(DEFAULT_RESULT);
192+
const [result, setResult] =
193+
React.useState<HookResult<ServerError, S, BAS, FVE, FBAVE, Data>>(DEFAULT_RESULT);
184194
const [input, setInput] = React.useState<InferIn<S>>();
185195
const [isExecuting, setIsExecuting] = React.useState(false);
186196

@@ -189,7 +199,7 @@ export const useOptimisticAction = <
189199
reducer
190200
);
191201

192-
const status = getActionStatus<ServerError, S, BAS, Data>({ isExecuting, result });
202+
const status = getActionStatus<ServerError, S, BAS, FVE, FBAVE, Data>({ isExecuting, result });
193203

194204
const execute = React.useCallback(
195205
(input: InferIn<S>) => {

‎packages/next-safe-action/src/hooks.types.ts

+23-8
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,39 @@ import type { MaybePromise } from "./utils";
44

55
/**
66
* Type of `result` object returned by `useAction` and `useOptimisticAction` hooks.
7+
* If a server-client communication error occurs, `fetchError` will be set to the error message.
78
*/
89
export type HookResult<
910
ServerError,
1011
S extends Schema,
11-
BAS extends Schema[],
12+
BAS extends readonly Schema[],
13+
FVE,
14+
FBAVE,
1215
Data,
13-
> = SafeActionResult<ServerError, S, BAS, Data> & {
16+
> = SafeActionResult<ServerError, S, BAS, FVE, FBAVE, Data> & {
1417
fetchError?: string;
1518
};
1619

1720
/**
1821
* Type of hooks callbacks. These are executed when action is in a specific state.
1922
*/
20-
export type HookCallbacks<ServerError, S extends Schema, BAS extends Schema[], Data> = {
23+
export type HookCallbacks<
24+
ServerError,
25+
S extends Schema,
26+
BAS extends readonly Schema[],
27+
FVE,
28+
FBAVE,
29+
Data,
30+
> = {
2131
onExecute?: (args: { input: InferIn<S> }) => MaybePromise<void>;
2232
onSuccess?: (args: { data: Data; input: InferIn<S>; reset: () => void }) => MaybePromise<void>;
2333
onError?: (args: {
24-
error: Omit<HookResult<ServerError, S, BAS, Data>, "data">;
34+
error: Omit<HookResult<ServerError, S, BAS, FVE, FBAVE, Data>, "data">;
2535
input: InferIn<S>;
2636
reset: () => void;
2737
}) => MaybePromise<void>;
2838
onSettled?: (args: {
29-
result: HookResult<ServerError, S, BAS, Data>;
39+
result: HookResult<ServerError, S, BAS, FVE, FBAVE, Data>;
3040
input: InferIn<S>;
3141
reset: () => void;
3242
}) => MaybePromise<void>;
@@ -36,9 +46,14 @@ export type HookCallbacks<ServerError, S extends Schema, BAS extends Schema[], D
3646
* Type of the safe action function passed to hooks. Same as `SafeActionFn` except it accepts
3747
* just a single input, without bind arguments.
3848
*/
39-
export type HookSafeActionFn<ServerError, S extends Schema, BAS extends Schema[], Data> = (
40-
clientInput: InferIn<S>
41-
) => Promise<SafeActionResult<ServerError, S, BAS, Data>>;
49+
export type HookSafeActionFn<
50+
ServerError,
51+
S extends Schema,
52+
BAS extends readonly Schema[],
53+
FVE,
54+
FBAVE,
55+
Data,
56+
> = (clientInput: InferIn<S>) => Promise<SafeActionResult<ServerError, S, BAS, FVE, FBAVE, Data>>;
4257

4358
/**
4459
* Type of the action status returned by `useAction` and `useOptimisticAction` hooks.

0 commit comments

Comments
 (0)
Please sign in to comment.