diff --git a/packages/example-app/src/app/login-action.ts b/packages/example-app/src/app/login-action.ts index 3002d83a..977c1108 100644 --- a/packages/example-app/src/app/login-action.ts +++ b/packages/example-app/src/app/login-action.ts @@ -8,13 +8,11 @@ const input = z.object({ password: z.string().min(8).max(100), }); -export const loginUser = action(input, async ({ username, password }) => { +export const loginUser = action(input, async ({ username, password }, _ctx, serverValidationError) => { if (username === "johndoe") { - return { - error: { - reason: "user_suspended", - }, - }; + serverValidationError({ + username: ['user_suspended'], + }); } if (username === "user" && password === "password") { @@ -23,9 +21,7 @@ export const loginUser = action(input, async ({ username, password }) => { }; } - return { - error: { - reason: "incorrect_credentials", - }, - }; + serverValidationError({ + username: ['incorrect_credentials'], + }); }); diff --git a/packages/next-safe-action/src/index.ts b/packages/next-safe-action/src/index.ts index f74b809f..ea862e6d 100644 --- a/packages/next-safe-action/src/index.ts +++ b/packages/next-safe-action/src/index.ts @@ -2,8 +2,8 @@ import type { Infer, InferIn, Schema } from "@decs/typeschema"; import { wrap } from "@decs/typeschema"; import { isNotFoundError } from "next/dist/client/components/not-found.js"; import { isRedirectError } from "next/dist/client/components/redirect.js"; -import type { MaybePromise } from "./utils"; -import { buildValidationErrors, isError } from "./utils"; +import type { MaybePromise, ValidationErrors } from "./utils"; +import { buildValidationErrors, isError, isServerValidationError } from "./utils"; // TYPES @@ -22,7 +22,7 @@ export type SafeClientOpts = { export type SafeAction = (input: InferIn) => Promise<{ data?: Data; serverError?: string; - validationErrors?: Partial | "_root", string[]>>; + validationErrors?: ValidationErrors; }>; /** @@ -30,13 +30,22 @@ export type SafeAction = (input: InferIn) => Promise< */ export type ServerCodeFn = ( parsedInput: Infer, - ctx: Context + ctx: Context, + serverValidationError: (validationErrors: ValidationErrors) => never, ) => Promise; // UTILS export const DEFAULT_SERVER_ERROR = "Something went wrong while executing the operation"; +export class ServerValidationError extends Error { + public validationErrors: ValidationErrors; + constructor(validationErrors: ValidationErrors) { + super("Server Validation Error"); + this.validationErrors = validationErrors; + } +} + // SAFE ACTION CLIENT /** @@ -69,6 +78,11 @@ export const createSafeActionClient = (createOpts?: SafeClientOpts ): SafeAction => { + // Helper function to create ServerValidationError with injected schema + const serverValidationError = (validationErrors: ValidationErrors) => { + throw new ServerValidationError(validationErrors); + } + // This is the function called by client. If `input` fails the schema // parsing, the function will return a `validationError` object, containing // all the invalid fields provided. @@ -88,7 +102,7 @@ export const createSafeActionClient = (createOpts?: SafeClientOpts(createOpts?: SafeClientOpts>(e)) { + return { validationErrors: e.validationErrors }; + } + // 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); diff --git a/packages/next-safe-action/src/utils.ts b/packages/next-safe-action/src/utils.ts index bcdf257e..42910880 100644 --- a/packages/next-safe-action/src/utils.ts +++ b/packages/next-safe-action/src/utils.ts @@ -1,11 +1,15 @@ import type { Infer, Schema, ValidationIssue } from "@decs/typeschema"; +import { ServerValidationError } from "./index"; + +export const isError = (error: unknown): error is Error => error instanceof Error; +export const isServerValidationError = (error: unknown): error is ServerValidationError => error instanceof ServerValidationError; -export const isError = (error: any): error is Error => error instanceof Error; export type MaybePromise = Promise | T; +export type ValidationErrors = Partial | "_root", string[]>>; // This function is used to build the validation errors object from a list of validation issues. export const buildValidationErrors = (issues: ValidationIssue[]) => { - const validationErrors = {} as Partial | "_root", string[]>>; + const validationErrors = {} as ValidationErrors; const appendIssue = (path: keyof Infer | "_root", message: string) => { if (validationErrors[path]?.length) { @@ -29,4 +33,4 @@ export const buildValidationErrors = (issues: Validation } return validationErrors; -}; +}; \ No newline at end of file diff --git a/packages/next-safe-action/src/zod.ts b/packages/next-safe-action/src/zod.ts index a926099d..8f8aebfb 100644 --- a/packages/next-safe-action/src/zod.ts +++ b/packages/next-safe-action/src/zod.ts @@ -1,8 +1,10 @@ import { isNotFoundError } from "next/dist/client/components/not-found.js"; import { isRedirectError } from "next/dist/client/components/redirect.js"; import type { z } from "zod"; -import type { SafeAction, SafeClientOpts, ServerCodeFn } from "."; -import { isError } from "./utils"; +import type {SafeAction, SafeClientOpts, ServerCodeFn} from "."; +import { ServerValidationError} from "."; +import type { ValidationErrors } from "./utils"; +import { isError, isServerValidationError } from "./utils"; // UTILS @@ -40,6 +42,11 @@ export const createSafeActionClient = (createOpts?: SafeClientOpts ): SafeAction => { + // Helper function to create ServerValidationError with injected schema + const serverValidationError = (validationErrors: ValidationErrors) => { + throw new ServerValidationError(validationErrors); + } + // This is the function called by client. If `input` fails the schema // parsing, the function will return a `validationError` object, containing // all the invalid fields provided. @@ -63,7 +70,7 @@ export const createSafeActionClient = (createOpts?: SafeClientOpts(createOpts?: SafeClientOpts>(e)) { + return { validationErrors: e.validationErrors }; + } + // 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);