Skip to content

Commit

Permalink
feat(validation-errors): support setting validation errors in action'…
Browse files Browse the repository at this point in the history
…s server code function (#52)

In some cases it's very useful, if not essential, to set custom validation errors during action's server code execution. This commit adds a function called `returnValidationErrors` that allows you to set custom validation errors when defining actions. Big thanks to @theboxer for the implementation.
  • Loading branch information
theboxer committed Feb 2, 2024
1 parent 6ab9b5c commit 64fb643
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 12 deletions.
19 changes: 10 additions & 9 deletions packages/example-app/src/app/login-action.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
"use server";

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

const input = z.object({
username: z.string().min(3).max(10),
password: z.string().min(8).max(100),
});

export const loginUser = action(input, async ({ username, password }) => {
export const loginUser = action(input, async ({ username, password }, ctx) => {
if (username === "johndoe") {
return {
error: {
reason: "user_suspended",
returnValidationErrors(input, {
username: {
_errors: ["user_suspended"],
},
};
});
}

if (username === "user" && password === "password") {
Expand All @@ -23,9 +24,9 @@ export const loginUser = action(input, async ({ username, password }) => {
};
}

return {
error: {
reason: "incorrect_credentials",
returnValidationErrors(input, {
username: {
_errors: ["incorrect_credentials"],
},
};
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const editUser = authAction(
// Here you have access to `userId`, which comes from `buildContext`
// return object in src/lib/safe-action.ts.
// \\\\\
async ({ fullName, age }, userId) => {
async ({ fullName, age }, { userId }) => {
if (fullName.toLowerCase() === "john doe") {
return {
error: {
Expand Down
2 changes: 1 addition & 1 deletion packages/example-app/src/lib/safe-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const authAction = createSafeActionClient({
parsedInput
);

return userId;
return { userId };
},
handleReturnedServerError,
});
31 changes: 31 additions & 0 deletions packages/next-safe-action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export const createSafeActionClient = <Context>(createOpts?: SafeClientOpts<Cont
throw e;
}

// If error is ServerValidationError, return validationErrors as if schema validation would fail.
if (e instanceof ServerValidationError) {
return { validationErrors: e.validationErrors as ValidationErrors<S> };
}

// If error cannot be handled, warn the user and return a generic message.
if (!isError(e)) {
console.warn("Could not handle server error. Not an instance of Error: ", e);
Expand All @@ -115,3 +120,29 @@ export const createSafeActionClient = <Context>(createOpts?: SafeClientOpts<Cont

return actionBuilder;
};

// VALIDATION ERRORS

// This class is internally used to throw validation errors in action's server code function, using
// `returnValidationErrors`.
class ServerValidationError<S extends Schema> extends Error {
public validationErrors: ValidationErrors<S>;
constructor(validationErrors: ValidationErrors<S>) {
super("Server Validation Error");
this.validationErrors = validationErrors;
}
}

/**
* Return custom validation errors to the client from the action's server code function.
* Code declared after this function invocation will not be executed.
* @param schema Input schema
* @param validationErrors Validation errors object
* @throws {ServerValidationError}
*/
export function returnValidationErrors<S extends Schema>(
schema: S,
validationErrors: ValidationErrors<S>
): never {
throw new ServerValidationError<S>(validationErrors);
}
2 changes: 1 addition & 1 deletion packages/next-safe-action/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Infer, Schema, ValidationIssue } from "@decs/typeschema";

export const isError = (error: any): error is Error => error instanceof Error;
export const isError = (error: unknown): error is Error => error instanceof Error;

// UTIL TYPES

Expand Down

0 comments on commit 64fb643

Please sign in to comment.