Skip to content

Commit

Permalink
feat(validation-errors): support customization of validation errors f…
Browse files Browse the repository at this point in the history
…ormat (#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
  • Loading branch information
TheEdoRan committed Apr 16, 2024
1 parent 218176d commit 66d4ea3
Show file tree
Hide file tree
Showing 21 changed files with 537 additions and 148 deletions.
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ const schema = z.object({
username: z.string().min(3).max(30),
});

const bindArgsSchemas: [userId: z.ZodString, age: z.ZodNumber] = [
z.string().uuid(),
z.number().min(18).max(150),
];

export const onboardUser = action
.metadata({ actionName: "onboardUser" })
.schema(schema)
.bindArgsSchemas<[userId: z.ZodString, age: z.ZodNumber]>([
z.string().uuid(),
z.number().min(18).max(150),
])
.bindArgsSchemas(bindArgsSchemas)
.action(
async ({
parsedInput: { username },
Expand Down
11 changes: 9 additions & 2 deletions packages/example-app/src/app/(examples)/direct/login-action.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"use server";

import { action } from "@/lib/safe-action";
import { returnValidationErrors } from "next-safe-action";
import {
flattenValidationErrors,
returnValidationErrors,
} from "next-safe-action";
import { z } from "zod";

const schema = z.object({
Expand All @@ -11,7 +14,11 @@ const schema = z.object({

export const loginUser = action
.metadata({ actionName: "loginUser" })
.schema(schema)
.schema(schema, {
// Here we use the `flattenValidationErrors` function to customize the returned validation errors
// object to the client.
formatValidationErrors: (ve) => flattenValidationErrors(ve).fieldErrors,
})
.action(async ({ parsedInput: { username, password } }) => {
if (username === "johndoe") {
returnValidationErrors(schema, {
Expand Down
3 changes: 2 additions & 1 deletion packages/example-app/src/lib/safe-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export const action = createSafeActionClient({
logObject.metadata = metadata;
logObject.result = result;

console.log("MIDDLEWARE LOG:", logObject);
console.log("LOGGING FROM MIDDLEWARE:");
console.dir(logObject, { depth: null });

// And then return the result of the awaited next middleware.
return result;
Expand Down
42 changes: 26 additions & 16 deletions packages/next-safe-action/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@ const DEFAULT_RESULT = {
fetchError: undefined,
serverError: undefined,
validationErrors: undefined,
} satisfies HookResult<any, any, any, any>;
} satisfies HookResult<any, any, any, any, any, any>;

const getActionStatus = <
const ServerError,
const S extends Schema,
const BAS extends Schema[],
const BAS extends readonly Schema[],
const FVE,
const FBAVE,
const Data,
>({
isExecuting,
result,
}: {
isExecuting: boolean;
result: HookResult<ServerError, S, BAS, Data>;
result: HookResult<ServerError, S, BAS, FVE, FBAVE, Data>;
}): HookActionStatus => {
if (isExecuting) {
return "executing";
Expand All @@ -48,7 +50,9 @@ const getActionStatus = <
const useActionCallbacks = <
const ServerError,
const S extends Schema,
const BAS extends Schema[],
const BAS extends readonly Schema[],
const FVE,
const FBAVE,
const Data,
>({
result,
Expand All @@ -57,11 +61,11 @@ const useActionCallbacks = <
reset,
cb,
}: {
result: HookResult<ServerError, S, BAS, Data>;
result: HookResult<ServerError, S, BAS, FVE, FBAVE, Data>;
input: InferIn<S>;
status: HookActionStatus;
reset: () => void;
cb?: HookCallbacks<ServerError, S, BAS, Data>;
cb?: HookCallbacks<ServerError, S, BAS, FVE, FBAVE, Data>;
}) => {
const onExecuteRef = React.useRef(cb?.onExecute);
const onSuccessRef = React.useRef(cb?.onSuccess);
Expand Down Expand Up @@ -107,18 +111,21 @@ const useActionCallbacks = <
export const useAction = <
const ServerError,
const S extends Schema,
const BAS extends Schema[],
const BAS extends readonly Schema[],
const FVE,
const FBAVE,
const Data,
>(
safeActionFn: HookSafeActionFn<ServerError, S, BAS, Data>,
callbacks?: HookCallbacks<ServerError, S, BAS, Data>
safeActionFn: HookSafeActionFn<ServerError, S, BAS, FVE, FBAVE, Data>,
callbacks?: HookCallbacks<ServerError, S, BAS, FVE, FBAVE, Data>
) => {
const [, startTransition] = React.useTransition();
const [result, setResult] = React.useState<HookResult<ServerError, S, BAS, Data>>(DEFAULT_RESULT);
const [result, setResult] =
React.useState<HookResult<ServerError, S, BAS, FVE, FBAVE, Data>>(DEFAULT_RESULT);
const [input, setInput] = React.useState<InferIn<S>>();
const [isExecuting, setIsExecuting] = React.useState(false);

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

const execute = React.useCallback(
(input: InferIn<S>) => {
Expand Down Expand Up @@ -171,16 +178,19 @@ export const useAction = <
export const useOptimisticAction = <
const ServerError,
const S extends Schema,
const BAS extends Schema[],
const BAS extends readonly Schema[],
const FVE,
const FBAVE,
const Data,
>(
safeActionFn: HookSafeActionFn<ServerError, S, BAS, Data>,
safeActionFn: HookSafeActionFn<ServerError, S, BAS, FVE, FBAVE, Data>,
initialOptimisticData: Data,
reducer: (state: Data, input: InferIn<S>) => Data,
callbacks?: HookCallbacks<ServerError, S, BAS, Data>
callbacks?: HookCallbacks<ServerError, S, BAS, FVE, FBAVE, Data>
) => {
const [, startTransition] = React.useTransition();
const [result, setResult] = React.useState<HookResult<ServerError, S, BAS, Data>>(DEFAULT_RESULT);
const [result, setResult] =
React.useState<HookResult<ServerError, S, BAS, FVE, FBAVE, Data>>(DEFAULT_RESULT);
const [input, setInput] = React.useState<InferIn<S>>();
const [isExecuting, setIsExecuting] = React.useState(false);

Expand All @@ -189,7 +199,7 @@ export const useOptimisticAction = <
reducer
);

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

const execute = React.useCallback(
(input: InferIn<S>) => {
Expand Down
31 changes: 23 additions & 8 deletions packages/next-safe-action/src/hooks.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,39 @@ import type { MaybePromise } from "./utils";

/**
* Type of `result` object returned by `useAction` and `useOptimisticAction` hooks.
* If a server-client communication error occurs, `fetchError` will be set to the error message.
*/
export type HookResult<
ServerError,
S extends Schema,
BAS extends Schema[],
BAS extends readonly Schema[],
FVE,
FBAVE,
Data,
> = SafeActionResult<ServerError, S, BAS, Data> & {
> = SafeActionResult<ServerError, S, BAS, FVE, FBAVE, Data> & {
fetchError?: string;
};

/**
* Type of hooks callbacks. These are executed when action is in a specific state.
*/
export type HookCallbacks<ServerError, S extends Schema, BAS extends Schema[], Data> = {
export type HookCallbacks<
ServerError,
S extends Schema,
BAS extends readonly Schema[],
FVE,
FBAVE,
Data,
> = {
onExecute?: (args: { input: InferIn<S> }) => MaybePromise<void>;
onSuccess?: (args: { data: Data; input: InferIn<S>; reset: () => void }) => MaybePromise<void>;
onError?: (args: {
error: Omit<HookResult<ServerError, S, BAS, Data>, "data">;
error: Omit<HookResult<ServerError, S, BAS, FVE, FBAVE, Data>, "data">;
input: InferIn<S>;
reset: () => void;
}) => MaybePromise<void>;
onSettled?: (args: {
result: HookResult<ServerError, S, BAS, Data>;
result: HookResult<ServerError, S, BAS, FVE, FBAVE, Data>;
input: InferIn<S>;
reset: () => void;
}) => MaybePromise<void>;
Expand All @@ -36,9 +46,14 @@ export type HookCallbacks<ServerError, S extends Schema, BAS extends Schema[], D
* Type of the safe action function passed to hooks. Same as `SafeActionFn` except it accepts
* just a single input, without bind arguments.
*/
export type HookSafeActionFn<ServerError, S extends Schema, BAS extends Schema[], Data> = (
clientInput: InferIn<S>
) => Promise<SafeActionResult<ServerError, S, BAS, Data>>;
export type HookSafeActionFn<
ServerError,
S extends Schema,
BAS extends readonly Schema[],
FVE,
FBAVE,
Data,
> = (clientInput: InferIn<S>) => Promise<SafeActionResult<ServerError, S, BAS, FVE, FBAVE, Data>>;

/**
* Type of the action status returned by `useAction` and `useOptimisticAction` hooks.
Expand Down
Loading

0 comments on commit 66d4ea3

Please sign in to comment.