diff --git a/packages/example-app/src/lib/safe-action.ts b/packages/example-app/src/lib/safe-action.ts index 9f134e0d..a33d11a2 100644 --- a/packages/example-app/src/lib/safe-action.ts +++ b/packages/example-app/src/lib/safe-action.ts @@ -1,5 +1,8 @@ import { randomUUID } from "crypto"; -import { DEFAULT_SERVER_ERROR, createSafeActionClient } from "next-safe-action"; +import { + DEFAULT_SERVER_ERROR_MESSAGE, + createSafeActionClient, +} from "next-safe-action"; export class ActionError extends Error {} @@ -20,7 +23,7 @@ export const action = createSafeActionClient({ } // Otherwise return default error message. - return DEFAULT_SERVER_ERROR; + return DEFAULT_SERVER_ERROR_MESSAGE; }, }).use(async ({ next, metadata }) => { // Here we use a logging middleware. diff --git a/packages/next-safe-action/src/hooks.ts b/packages/next-safe-action/src/hooks.ts index 1c766e3f..b9509c99 100644 --- a/packages/next-safe-action/src/hooks.ts +++ b/packages/next-safe-action/src/hooks.ts @@ -16,11 +16,11 @@ const DEFAULT_RESULT = { fetchError: undefined, serverError: undefined, validationErrors: undefined, -} satisfies HookResult; +} satisfies HookResult; -const getActionStatus = ( +const getActionStatus = ( isExecuting: boolean, - result: HookResult + result: HookResult ): HookActionStatus => { if (isExecuting) { return "executing"; @@ -37,12 +37,12 @@ const getActionStatus = ( return "idle"; }; -const useActionCallbacks = ( - result: HookResult, +const useActionCallbacks = ( + result: HookResult, input: InferIn, status: HookActionStatus, reset: () => void, - cb?: HookCallbacks + cb?: HookCallbacks ) => { const onExecuteRef = React.useRef(cb?.onExecute); const onSuccessRef = React.useRef(cb?.onSuccess); @@ -85,16 +85,16 @@ const useActionCallbacks = ( * * {@link https://next-safe-action.dev/docs/usage/client-components/hooks/useaction See an example} */ -export const useAction = ( - safeActionFn: SafeActionFn, - callbacks?: HookCallbacks +export const useAction = ( + safeActionFn: SafeActionFn, + callbacks?: HookCallbacks ) => { const [, startTransition] = React.useTransition(); - const [result, setResult] = React.useState>(DEFAULT_RESULT); + const [result, setResult] = React.useState>(DEFAULT_RESULT); const [input, setInput] = React.useState>(); const [isExecuting, setIsExecuting] = React.useState(false); - const status = getActionStatus(isExecuting, result); + const status = getActionStatus(isExecuting, result); const execute = React.useCallback( (input: InferIn) => { @@ -144,14 +144,14 @@ export const useAction = ( * * {@link https://next-safe-action.dev/docs/usage/client-components/hooks/useoptimisticaction See an example} */ -export const useOptimisticAction = ( - safeActionFn: SafeActionFn, +export const useOptimisticAction = ( + safeActionFn: SafeActionFn, initialOptimisticData: Data, reducer: (state: Data, input: InferIn) => Data, - callbacks?: HookCallbacks + callbacks?: HookCallbacks ) => { const [, startTransition] = React.useTransition(); - const [result, setResult] = React.useState>(DEFAULT_RESULT); + const [result, setResult] = React.useState>(DEFAULT_RESULT); const [input, setInput] = React.useState>(); const [isExecuting, setIsExecuting] = React.useState(false); @@ -160,7 +160,7 @@ export const useOptimisticAction = ( reducer ); - const status = getActionStatus(isExecuting, result); + const status = getActionStatus(isExecuting, result); const execute = React.useCallback( (input: InferIn) => { diff --git a/packages/next-safe-action/src/hooks.types.ts b/packages/next-safe-action/src/hooks.types.ts index 4e83cb3c..cbda8016 100644 --- a/packages/next-safe-action/src/hooks.types.ts +++ b/packages/next-safe-action/src/hooks.types.ts @@ -5,23 +5,27 @@ import type { MaybePromise } from "./utils"; /** * Type of `result` object returned by `useAction` and `useOptimisticAction` hooks. */ -export type HookResult = SafeActionResult & { +export type HookResult = SafeActionResult< + ServerError, + S, + Data +> & { fetchError?: string; }; /** * Type of hooks callbacks. These are executed when action is in a specific state. */ -export type HookCallbacks = { +export type HookCallbacks = { onExecute?: (input: InferIn) => MaybePromise; onSuccess?: (data: Data, input: InferIn, reset: () => void) => MaybePromise; onError?: ( - error: Omit, "data">, + error: Omit, "data">, input: InferIn, reset: () => void ) => MaybePromise; onSettled?: ( - result: HookResult, + result: HookResult, input: InferIn, reset: () => void ) => MaybePromise; diff --git a/packages/next-safe-action/src/index.ts b/packages/next-safe-action/src/index.ts index b70f0878..382f0963 100644 --- a/packages/next-safe-action/src/index.ts +++ b/packages/next-safe-action/src/index.ts @@ -11,7 +11,7 @@ import type { SafeActionResult, ServerCodeFn, } from "./index.types"; -import { DEFAULT_SERVER_ERROR, isError } from "./utils"; +import { DEFAULT_SERVER_ERROR_MESSAGE, isError } from "./utils"; import { ServerValidationError, buildValidationErrors, @@ -19,17 +19,21 @@ import { } from "./validation-errors"; import type { ValidationErrors } from "./validation-errors.types"; -class SafeActionClient { - private readonly handleServerErrorLog: NonNullable; +class SafeActionClient { + private readonly handleServerErrorLog: NonNullable< + SafeActionClientOpts["handleServerErrorLog"] + >; private readonly handleReturnedServerError: NonNullable< - SafeActionClientOpts["handleReturnedServerError"] + SafeActionClientOpts["handleReturnedServerError"] >; - private middlewareFns: MiddlewareFn[]; + private middlewareFns: MiddlewareFn[]; private _metadata: ActionMetadata = {}; constructor( - opts: { middlewareFns: MiddlewareFn[] } & Required + opts: { middlewareFns: MiddlewareFn[] } & Required< + SafeActionClientOpts + > ) { this.middlewareFns = opts.middlewareFns; this.handleServerErrorLog = opts.handleServerErrorLog; @@ -42,7 +46,7 @@ class SafeActionClient { * @returns {SafeActionClient} */ public clone() { - return new SafeActionClient({ + return new SafeActionClient({ handleReturnedServerError: this.handleReturnedServerError, handleServerErrorLog: this.handleServerErrorLog, middlewareFns: [...this.middlewareFns], // copy the middleware stack so we don't mutate it @@ -55,11 +59,11 @@ class SafeActionClient { * @returns SafeActionClient */ public use( - middlewareFn: MiddlewareFn + middlewareFn: MiddlewareFn ) { this.middlewareFns.push(middlewareFn); - return new SafeActionClient({ + return new SafeActionClient({ middlewareFns: this.middlewareFns, handleReturnedServerError: this.handleReturnedServerError, handleServerErrorLog: this.handleServerErrorLog, @@ -93,11 +97,13 @@ class SafeActionClient { * @param serverCodeFn A function that executes the server code. * @returns {SafeActionFn} */ - define(serverCodeFn: ServerCodeFn): SafeActionFn { + define( + serverCodeFn: ServerCodeFn + ): SafeActionFn { return async (clientInput: unknown) => { let prevCtx: any = null; let frameworkError: Error | undefined = undefined; - const middlewareResult: MiddlewareResult = { success: false }; + const middlewareResult: MiddlewareResult = { success: false }; // Execute the middleware stack. const executeMiddlewareChain = async (idx = 0) => { @@ -149,16 +155,14 @@ class SafeActionClient { return; } - if (!isError(e)) { - console.warn("Could not handle server error. Not an instance of Error: ", e); - middlewareResult.serverError = DEFAULT_SERVER_ERROR; - return; - } + // If error is not an instance of Error, wrap it in an Error object with + // the default message. + const error = isError(e) ? e : new Error(DEFAULT_SERVER_ERROR_MESSAGE); - await Promise.resolve(classThis.handleServerErrorLog(e)); + await Promise.resolve(classThis.handleServerErrorLog(error)); middlewareResult.serverError = await Promise.resolve( - classThis.handleReturnedServerError(e) + classThis.handleReturnedServerError(error) ); } }; @@ -170,7 +174,7 @@ class SafeActionClient { throw frameworkError; } - const actionResult: SafeActionResult = {}; + const actionResult: SafeActionResult = {}; if (typeof middlewareResult.data !== "undefined") { actionResult.data = middlewareResult.data as Data; @@ -199,7 +203,9 @@ class SafeActionClient { * * {@link https://next-safe-action.dev/docs/getting-started See an example} */ -export const createSafeActionClient = (createOpts?: SafeActionClientOpts) => { +export const createSafeActionClient = ( + createOpts?: SafeActionClientOpts +) => { // If server log function is not provided, default to `console.error` for logging // server error messages. const handleServerErrorLog = @@ -211,17 +217,19 @@ export const createSafeActionClient = (createOpts?: SafeActionClientOpts) => { // If `handleReturnedServerError` is provided, use it to handle server error // messages returned on the client. // Otherwise mask the error and use a generic message. - const handleReturnedServerError = (e: Error) => - createOpts?.handleReturnedServerError?.(e) || DEFAULT_SERVER_ERROR; + const handleReturnedServerError = ((e: Error) => + createOpts?.handleReturnedServerError?.(e) || DEFAULT_SERVER_ERROR_MESSAGE) as NonNullable< + SafeActionClientOpts["handleReturnedServerError"] + >; - return new SafeActionClient({ + return new SafeActionClient({ middlewareFns: [async ({ next }) => next({ ctx: null })], handleServerErrorLog, handleReturnedServerError, }); }; -export { DEFAULT_SERVER_ERROR, returnValidationErrors, type ValidationErrors }; +export { DEFAULT_SERVER_ERROR_MESSAGE, returnValidationErrors, type ValidationErrors }; export type { ActionMetadata, diff --git a/packages/next-safe-action/src/index.types.ts b/packages/next-safe-action/src/index.types.ts index 66902b85..2ffa7158 100644 --- a/packages/next-safe-action/src/index.types.ts +++ b/packages/next-safe-action/src/index.types.ts @@ -5,11 +5,28 @@ import type { ValidationErrors } from "./validation-errors.types"; /** * Type of options when creating a new safe action client. */ -export type SafeActionClientOpts = { +export type SafeActionClientOpts = { handleServerErrorLog?: (e: Error) => MaybePromise; - handleReturnedServerError?: (e: Error) => MaybePromise; + handleReturnedServerError?: (e: Error) => MaybePromise; }; +/** + * Type of the result of a safe action. + */ +// eslint-disable-next-line +export type SafeActionResult = { + data?: Data; + serverError?: ServerError; + validationErrors?: ValidationErrors; +}; + +/** + * Type of the function called from components with typesafe input data. + */ +export type SafeActionFn = ( + input: InferIn +) => Promise>; + /** * Type of meta options to be passed when defining a new safe action. */ @@ -21,7 +38,12 @@ export type ActionMetadata = { * Type of the result of a middleware function. It extends the result of a safe action with * `parsedInput` and `ctx` optional properties. */ -export type MiddlewareResult = SafeActionResult & { +export type MiddlewareResult = SafeActionResult< + ServerError, + any, + unknown, + NextCtx +> & { parsedInput?: unknown; ctx?: unknown; success: boolean; @@ -30,15 +52,15 @@ export type MiddlewareResult = SafeActionResult /** * Type of the middleware function passed to a safe action client. */ -export type MiddlewareFn = { +export type MiddlewareFn = { (opts: { clientInput: ClientInput; ctx: Ctx; metadata: ActionMetadata; next: { - (opts: { ctx: NC }): Promise>; + (opts: { ctx: NC }): Promise>; }; - }): Promise>; + }): Promise>; }; /** @@ -48,20 +70,3 @@ export type ServerCodeFn = ( parsedInput: Infer, utils: { ctx: Context; metadata: ActionMetadata } ) => Promise; - -/** - * Type of the result of a safe action. - */ -// eslint-disable-next-line -export type SafeActionResult = { - data?: Data; - serverError?: string; - validationErrors?: ValidationErrors; -}; - -/** - * Type of the function called from components with typesafe input data. - */ -export type SafeActionFn = ( - input: InferIn -) => Promise>; diff --git a/packages/next-safe-action/src/utils.ts b/packages/next-safe-action/src/utils.ts index 6aae2adf..445b86b5 100644 --- a/packages/next-safe-action/src/utils.ts +++ b/packages/next-safe-action/src/utils.ts @@ -1,4 +1,4 @@ -export const DEFAULT_SERVER_ERROR = "Something went wrong while executing the operation."; +export const DEFAULT_SERVER_ERROR_MESSAGE = "Something went wrong while executing the operation."; export const isError = (error: unknown): error is Error => error instanceof Error; diff --git a/website/docs/safe-action-client/index.md b/website/docs/safe-action-client/index.md index 9b4a06ca..280bcb0e 100644 --- a/website/docs/safe-action-client/index.md +++ b/website/docs/safe-action-client/index.md @@ -13,5 +13,5 @@ Here's a reference of all the available optional functions: | Function name | Purpose | |------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `handleReturnedServerError?` | When an error occurs on the server after executing the action on the client, it lets you define custom logic to returns a custom `serverError` message instead of the default one. More information [here](/docs/safe-action-client/initialization-options#handlereturnedservererror). | +| `handleReturnedServerError?` | When an error occurs on the server after executing the action on the client, it lets you define custom logic to returns a custom `serverError` instead of the default string. More information [here](/docs/safe-action-client/initialization-options#handlereturnedservererror). | | `handleServerErrorLog?` | When an error occurs on the server after executing the action on the client, it lets you define custom logic to log the error on the server. By default the error is logged via `console.error`. More information [here](/docs/safe-action-client/initialization-options#handleservererrorlog). | \ No newline at end of file diff --git a/website/docs/safe-action-client/initialization-options.md b/website/docs/safe-action-client/initialization-options.md index ce4fef75..897374d4 100644 --- a/website/docs/safe-action-client/initialization-options.md +++ b/website/docs/safe-action-client/initialization-options.md @@ -7,9 +7,9 @@ description: You can initialize a safe action client with these options. ## `handleReturnedServerError?` -You can provide this optional function to the safe action client. It is used to customize the server error message returned to the client, if one occurs during action's server execution. This includes errors thrown by the action server code, and errors thrown by the middleware. +You can provide this optional function to the safe action client. It is used to customize the server error returned to the client, if one occurs during action's server execution. This includes errors thrown by the action server code, and errors thrown by the middleware. -Here's a simple example, changing the message for every error thrown on the server: +Here's a simple example, changing the default message for every error thrown on the server: ```typescript title=src/lib/safe-action.ts export const actionClient = createSafeActionClient({ @@ -25,7 +25,7 @@ export const actionClient = createSafeActionClient({ A more useful one would be to customize the message based on the error type. We can, for instance, create a custom error class and check the error type inside this function: ```typescript title=src/lib/safe-action.ts -import { DEFAULT_SERVER_ERROR } from "next-safe-action"; +import { DEFAULT_SERVER_ERROR_MESSAGE } from "next-safe-action"; class MyCustomError extends Error {} @@ -39,11 +39,13 @@ export const actionClient = createSafeActionClient({ } // Every other error that occurs will be masked with the default message. - return DEFAULT_SERVER_ERROR; + return DEFAULT_SERVER_ERROR_MESSAGE; }, }); ``` +Note that the return type of this function will determine the type of the server error that will be returned to the client. By default it is a string with the `DEFAULT_SERVER_ERROR_MESSAGE` for all errors. + ## `handleServerErrorLog?` You can provide this optional function to the safe action client. This is used to define how errors should be logged when one occurs while the server is executing an action. This includes errors thrown by the action server code, and errors thrown by the middleware. Here you get as argument the **original error object**, not a message customized by `handleReturnedServerError`, if provided. diff --git a/website/docs/types.md b/website/docs/types.md index 5425c617..e8902eaf 100644 --- a/website/docs/types.md +++ b/website/docs/types.md @@ -12,12 +12,34 @@ description: List of next-safe-action types. Type of options when creating a new safe action client. ```typescript -export type SafeActionClientOpts = { +export type SafeActionClientOpts = { handleServerErrorLog?: (e: Error) => MaybePromise; - handleReturnedServerError?: (e: Error) => MaybePromise; + handleReturnedServerError?: (e: Error) => MaybePromise; }; ``` +### `SafeActionResult` + +Type of the result of a safe action. + +```typescript +export type SafeActionResult = { + data?: Data; + serverError?: ServerError; + validationErrors?: ValidationErrors; +}; +``` + +### `SafeActionFn` + +Type of the function called from components with typesafe input data. + +```typescript +export type SafeActionFn = ( + input: InferIn +) => Promise>; +``` + ### `ActionMetadata` Type of meta options to be passed when defining a new safe action. @@ -33,10 +55,15 @@ export type ActionMetadata = { Type of the result of a middleware function. It extends the result of a safe action with `parsedInput` and `ctx` optional properties. ```typescript -export type MiddlewareResult = SafeActionResult & { - parsedInput?: unknown; - ctx?: unknown; - success: boolean; +export type MiddlewareResult = SafeActionResult< + ServerError, + any, + unknown, + NextCtx +> & { + parsedInput?: unknown; + ctx?: unknown; + success: boolean; }; ``` @@ -45,15 +72,15 @@ export type MiddlewareResult = SafeActionResult Type of the middleware function passed to a safe action client. ```typescript -export type MiddlewareFn = { - (opts: { - clientInput: ClientInput; - ctx: Ctx; - metadata: ActionMetadata; - next: { - (opts: { ctx: NC }): Promise>; - }; - }): Promise>; +export type MiddlewareFn = { + (opts: { + clientInput: ClientInput; + ctx: Ctx; + metadata: ActionMetadata; + next: { + (opts: { ctx: NC }): Promise>; + }; + }): Promise>; }; ``` @@ -76,28 +103,6 @@ Type of the returned object when input validation fails. export type ValidationErrors = Extend>>; ``` -### `SafeActionResult` - -Type of the result of a safe action. - -```typescript -export type SafeActionResult = { - data?: Data; - serverError?: string; - validationErrors?: ValidationErrors; -}; -``` - -### `SafeActionFn` - -Type of the function called from components with typesafe input data. - -```typescript -export type SafeActionFn = ( - input: InferIn -) => Promise>; -``` - ## /hooks ### `HookResult` @@ -107,8 +112,12 @@ 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. ```typescript -type HookResult = SafeActionResult & { - fetchError?: string; +export type HookResult = SafeActionResult< + ServerError, + S, + Data +> & { + fetchError?: string; }; ``` @@ -117,19 +126,19 @@ type HookResult = SafeActionResult & { Type of hooks callbacks. These are executed when action is in a specific state. ```typescript -type HookCallbacks = { - onExecute?: (input: InferIn) => MaybePromise; - onSuccess?: (data: Data, input: InferIn, reset: () => void) => MaybePromise; - onError?: ( - error: Omit, "data">, - input: InferIn, - reset: () => void - ) => MaybePromise; - onSettled?: ( - result: HookResult, - input: InferIn, - reset: () => void - ) => MaybePromise; +export type HookCallbacks = { + onExecute?: (input: InferIn) => MaybePromise; + onSuccess?: (data: Data, input: InferIn, reset: () => void) => MaybePromise; + onError?: ( + error: Omit, "data">, + input: InferIn, + reset: () => void + ) => MaybePromise; + onSettled?: ( + result: HookResult, + input: InferIn, + reset: () => void + ) => MaybePromise; }; ``` diff --git a/website/docs/usage/action-result-object.md b/website/docs/usage/action-result-object.md index d4293417..3a829d6d 100644 --- a/website/docs/usage/action-result-object.md +++ b/website/docs/usage/action-result-object.md @@ -12,4 +12,4 @@ Here's how action result object is structured (all keys are optional): |--------------------|--------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `data?` | Execution is successful. | What you returned in action's server code. | | `validationErrors?` | Input data doesn't pass validation. | An object whose keys are the names of the fields that failed validation. Each key's value is either an `ErrorList` or a nested key with an `ErrorList` inside.
`ErrorList` is defined as: `{ errors?: string[] }`.
It follows the same structure as [Zod's `format` function](https://zod.dev/ERROR_HANDLING?id=formatting-errors). -| `serverError?` | An error occurs during action's server code execution. | A `string` that by default is "Something went wrong while executing the operation" for every server error that occurs, but this is [configurable](/docs/safe-action-client/initialization-options#handlereturnedservererror) when instantiating a new client. | \ No newline at end of file +| `serverError?` | An error occurs during action's server code execution. | A generic type that by default is the string "Something went wrong while executing the operation." for every server error that occurs, but this is [configurable](/docs/safe-action-client/initialization-options#handlereturnedservererror) when instantiating a new client. | \ No newline at end of file diff --git a/website/docs/usage/middleware.md b/website/docs/usage/middleware.md index e419fd18..93c6d9b2 100644 --- a/website/docs/usage/middleware.md +++ b/website/docs/usage/middleware.md @@ -20,7 +20,7 @@ Instance level is the right place when you want to share middleware behavior for Here we'll use a logging middleware in the base client and then extend it with an authorization middleware in `authActionClient`. We'll also define a safe action called `editProfile`, that will use `authActionClient` as its client. Note that the `handleReturnedServerError` function passed to the base client will also be used for `authActionClient`: ```typescript title="src/lib/safe-action.ts" -import { createSafeActionClient } from "next-safe-action"; +import { createSafeActionClient, DEFAULT_SERVER_ERROR_MESSAGE } from "next-safe-action"; import { cookies } from "next/headers"; import { getUserIdFromSessionId } from "./db"; @@ -33,7 +33,7 @@ const actionClient = createSafeActionClient({ return e.message; } - return DEFAULT_SERVER_ERROR; + return DEFAULT_SERVER_ERROR_MESSAGE; }, // Define logging middleware. }).use(async ({ next, clientInput, metadata }) => {