From f8fe12025e75291e89473380851d5569d96207bb Mon Sep 17 00:00:00 2001 From: Edoardo Ranghieri Date: Wed, 26 Jun 2024 02:12:44 +0200 Subject: [PATCH] refactor: pass util props to `handleReturnedServerError` and `handleServerErrorLog` (#184) This PR adds util props as the second argument of `handleReturnedServerError` and `handleServerErrorLog` optional init functions. Now you have access to `clientInput`, `bindArgsClientInputs`, `ctx`, `metadata` and also `returnedError` (just for `handleServerErrorLog`), which is the server error customized by `handleReturnedServerError` function. re #177 --- .../next-safe-action/src/action-builder.ts | 28 ++++++++++++++++--- packages/next-safe-action/src/index.ts | 17 +++++------ packages/next-safe-action/src/index.types.ts | 22 +++++++++++++-- .../src/safe-action-client.ts | 8 ++++-- packages/next-safe-action/src/typeschema.ts | 20 +++++++------ .../initialization-options.md | 19 +++++++++---- website/docs/types.md | 28 +++++++++++++++++-- 7 files changed, 107 insertions(+), 35 deletions(-) diff --git a/packages/next-safe-action/src/action-builder.ts b/packages/next-safe-action/src/action-builder.ts index d1e52f47..8c3cd0a8 100644 --- a/packages/next-safe-action/src/action-builder.ts +++ b/packages/next-safe-action/src/action-builder.ts @@ -41,8 +41,10 @@ export function actionBuilder< handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn; metadataSchema: MetadataSchema; metadata: MD; - handleServerErrorLog: NonNullable["handleServerErrorLog"]>; - handleReturnedServerError: NonNullable["handleReturnedServerError"]>; + handleServerErrorLog: NonNullable["handleServerErrorLog"]>; + handleReturnedServerError: NonNullable< + SafeActionClientOpts["handleReturnedServerError"] + >; middlewareFns: MiddlewareFn[]; ctxType: Ctx; validationStrategy: "typeschema" | "zod"; @@ -239,8 +241,26 @@ export function actionBuilder< // 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(args.handleServerErrorLog(error)); - middlewareResult.serverError = await Promise.resolve(args.handleReturnedServerError(error)); + const returnedError = await Promise.resolve( + args.handleReturnedServerError(error, { + clientInput: clientInputs.at(-1), // pass raw client input + bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], + ctx: prevCtx, + metadata: args.metadata as MetadataSchema extends Schema ? Infer : undefined, + }) + ); + + middlewareResult.serverError = returnedError; + + await Promise.resolve( + args.handleServerErrorLog(error, { + returnedError, + clientInput: clientInputs.at(-1), // pass raw client input + bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], + ctx: prevCtx, + metadata: args.metadata as MetadataSchema extends Schema ? Infer : undefined, + }) + ); } } }; diff --git a/packages/next-safe-action/src/index.ts b/packages/next-safe-action/src/index.ts index 63f43ab8..35125384 100644 --- a/packages/next-safe-action/src/index.ts +++ b/packages/next-safe-action/src/index.ts @@ -41,17 +41,18 @@ export const createSafeActionClient = < // server error messages. const handleServerErrorLog = createOpts?.handleServerErrorLog || - ((e) => { - console.error("Action error:", e.message); - }); + (((originalError: Error) => { + console.error("Action error:", originalError.message); + }) as unknown as NonNullable["handleServerErrorLog"]>); // 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_MESSAGE) as NonNullable< - SafeActionClientOpts["handleReturnedServerError"] - >; + const handleReturnedServerError = + createOpts?.handleReturnedServerError || + ((() => DEFAULT_SERVER_ERROR_MESSAGE) as unknown as NonNullable< + SafeActionClientOpts["handleReturnedServerError"] + >); return new SafeActionClient({ middlewareFns: [async ({ next }) => next({ ctx: undefined })], @@ -61,7 +62,7 @@ export const createSafeActionClient = < schemaFn: undefined, bindArgsSchemas: [], ctxType: undefined, - metadataSchema: createOpts?.defineMetadataSchema?.(), + metadataSchema: (createOpts?.defineMetadataSchema?.() ?? undefined) as MetadataSchema, metadata: undefined as MetadataSchema extends Schema ? Infer : undefined, defaultValidationErrorsShape: (createOpts?.defaultValidationErrorsShape ?? "formatted") as ODVES, throwValidationErrors: Boolean(createOpts?.throwValidationErrors), diff --git a/packages/next-safe-action/src/index.types.ts b/packages/next-safe-action/src/index.types.ts index 376f6565..41bc8d25 100644 --- a/packages/next-safe-action/src/index.types.ts +++ b/packages/next-safe-action/src/index.types.ts @@ -8,6 +8,16 @@ import type { BindArgsValidationErrors, ValidationErrors } from "./validation-er */ export type DVES = "formatted" | "flattened"; +/** + * Type of the util properties passed to server error handler functions. + */ +export type ServerErrorFunctionUtils = { + clientInput: unknown; + bindArgsClientInputs: unknown[]; + ctx: unknown; + metadata: MetadataSchema extends Schema ? Infer : undefined; +}; + /** * Type of options when creating a new safe action client. */ @@ -16,9 +26,17 @@ export type SafeActionClientOpts< MetadataSchema extends Schema | undefined, ODVES extends DVES | undefined, > = { - handleServerErrorLog?: (e: Error) => MaybePromise; - handleReturnedServerError?: (e: Error) => MaybePromise; defineMetadataSchema?: () => MetadataSchema; + handleReturnedServerError?: ( + error: Error, + utils: ServerErrorFunctionUtils + ) => MaybePromise; + handleServerErrorLog?: ( + originalError: Error, + utils: ServerErrorFunctionUtils & { + returnedError: ServerError; + } + ) => MaybePromise; throwValidationErrors?: boolean; defaultValidationErrorsShape?: ODVES; }; diff --git a/packages/next-safe-action/src/safe-action-client.ts b/packages/next-safe-action/src/safe-action-client.ts index 34d63789..7a85dc10 100644 --- a/packages/next-safe-action/src/safe-action-client.ts +++ b/packages/next-safe-action/src/safe-action-client.ts @@ -31,9 +31,11 @@ export class SafeActionClient< const CBAVE = undefined, > { readonly #validationStrategy: "typeschema" | "zod"; - readonly #handleServerErrorLog: NonNullable["handleServerErrorLog"]>; + readonly #handleServerErrorLog: NonNullable< + SafeActionClientOpts["handleServerErrorLog"] + >; readonly #handleReturnedServerError: NonNullable< - SafeActionClientOpts["handleReturnedServerError"] + SafeActionClientOpts["handleReturnedServerError"] >; readonly #middlewareFns: MiddlewareFn[]; readonly #ctxType = undefined as Ctx; @@ -59,7 +61,7 @@ export class SafeActionClient< ctxType: Ctx; } & Required< Pick< - SafeActionClientOpts, + SafeActionClientOpts, "handleReturnedServerError" | "handleServerErrorLog" | "defaultValidationErrorsShape" | "throwValidationErrors" > > diff --git a/packages/next-safe-action/src/typeschema.ts b/packages/next-safe-action/src/typeschema.ts index d650920c..3f61d916 100644 --- a/packages/next-safe-action/src/typeschema.ts +++ b/packages/next-safe-action/src/typeschema.ts @@ -24,7 +24,8 @@ export type * from "./validation-errors.types"; /** * Create a new safe action client. - * Note: this client only works with Zod as the validation library. + * Note: this client is for validation libraries other than Zod, and can cause some problems with deployments, check out + * [the troubleshooting page](https://next-safe-action.dev/docs/troubleshooting#typeschema-issues-with-edge-runtime) on the website. * If you want to use a validation library supported by [TypeSchema](https://typeschema.com), import this client from `/typeschema` path. * @param createOpts Optional initialization options * @@ -41,17 +42,18 @@ export const createSafeActionClient = < // server error messages. const handleServerErrorLog = createOpts?.handleServerErrorLog || - ((e) => { - console.error("Action error:", e.message); - }); + (((originalError: Error) => { + console.error("Action error:", originalError.message); + }) as unknown as NonNullable["handleServerErrorLog"]>); // 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_MESSAGE) as NonNullable< - SafeActionClientOpts["handleReturnedServerError"] - >; + const handleReturnedServerError = + createOpts?.handleReturnedServerError || + ((() => DEFAULT_SERVER_ERROR_MESSAGE) as unknown as NonNullable< + SafeActionClientOpts["handleReturnedServerError"] + >); return new SafeActionClient({ middlewareFns: [async ({ next }) => next({ ctx: undefined })], @@ -61,7 +63,7 @@ export const createSafeActionClient = < schemaFn: undefined, bindArgsSchemas: [], ctxType: undefined, - metadataSchema: createOpts?.defineMetadataSchema?.(), + metadataSchema: (createOpts?.defineMetadataSchema?.() ?? undefined) as MetadataSchema, metadata: undefined as MetadataSchema extends Schema ? Infer : undefined, defaultValidationErrorsShape: (createOpts?.defaultValidationErrorsShape ?? "formatted") as ODVES, throwValidationErrors: Boolean(createOpts?.throwValidationErrors), diff --git a/website/docs/safe-action-client/initialization-options.md b/website/docs/safe-action-client/initialization-options.md index 2b762eab..cebd119a 100644 --- a/website/docs/safe-action-client/initialization-options.md +++ b/website/docs/safe-action-client/initialization-options.md @@ -7,7 +7,7 @@ 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 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. You also have access to useful properties via the `utils` object, which is the second argument of the function. Here's a simple example, changing the default message for every error thrown on the server: @@ -16,7 +16,10 @@ import { createSafeActionClient } from "next-safe-action"; export const actionClient = createSafeActionClient({ // Can also be an async function. - handleReturnedServerError(e) { + handleReturnedServerError(e, utils) { + // You can access these properties inside the `utils` object. + const { clientInput, bindArgsClientInputs, metadata, ctx } = utils; + return "Oh no, something went wrong!"; }, }); @@ -66,7 +69,7 @@ Note that the return type of this function will determine the type of the server ## `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. +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 the first argument the **original error object**, not the one customized by `handleReturnedServerError`, if provided. Though, you can access the `returnedError` and other useful properties inside the `utils` object, which is the second argument. Here's a simple example, logging error to the console while also reporting it to an error handling system: @@ -75,12 +78,16 @@ import { createSafeActionClient } from "next-safe-action"; export const actionClient = createSafeActionClient({ // Can also be an async function. - handleServerErrorLog(e) { + handleServerErrorLog(originalError, utils) { + // You can access these properties inside the `utils` object. + // Note that here you also have access to the custom server error defined by `handleReturnedServerError`. + const { clientInput, bindArgsClientInputs, metadata, ctx, returnedError } = utils; + // We can, for example, also send the error to a dedicated logging system. - reportToErrorHandlingSystem(e); + reportToErrorHandlingSystem(originalError); // And also log it to the console. - console.error("Action error:", e.message); + console.error("Action error:", originalError.message); }, }); ``` diff --git a/website/docs/types.md b/website/docs/types.md index 57462a24..25346cfa 100644 --- a/website/docs/types.md +++ b/website/docs/types.md @@ -15,6 +15,19 @@ Type of the default validation errors shape passed to `createSafeActionClient` v export type DVES = "flattened" | "formatted"; ``` +### `ServerErrorFunctionUtils` + +Type of the util properties passed to server error handler functions. + +```typescript +export type ServerErrorFunctionUtils = { + clientInput: unknown; + bindArgsClientInputs: unknown[]; + ctx: unknown; + metadata: MetadataSchema extends Schema ? Infer : undefined; +}; +``` + ### `SafeActionClientOpts` Type of options when creating a new safe action client. @@ -23,11 +36,20 @@ Type of options when creating a new safe action client. export type SafeActionClientOpts< ServerError, MetadataSchema extends Schema | undefined, - ODVES extends DVES | undefined + ODVES extends DVES | undefined, > = { - handleServerErrorLog?: (e: Error) => MaybePromise; - handleReturnedServerError?: (e: Error) => MaybePromise; defineMetadataSchema?: () => MetadataSchema; + handleReturnedServerError?: ( + error: Error, + utils: ServerErrorFunctionUtils + ) => MaybePromise; + handleServerErrorLog?: ( + originalError: Error, + utils: ServerErrorFunctionUtils & { + returnedError: ServerError; + } + ) => MaybePromise; + throwValidationErrors?: boolean; defaultValidationErrorsShape?: ODVES; }; ```