Skip to content

Commit

Permalink
refactor: pass util props to handleReturnedServerError and `handleS…
Browse files Browse the repository at this point in the history
…erverErrorLog` (#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
  • Loading branch information
TheEdoRan authored Jun 26, 2024
1 parent b3e78bb commit f8fe120
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 35 deletions.
28 changes: 24 additions & 4 deletions packages/next-safe-action/src/action-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ export function actionBuilder<
handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<BAS, CBAVE>;
metadataSchema: MetadataSchema;
metadata: MD;
handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, any, any>["handleServerErrorLog"]>;
handleReturnedServerError: NonNullable<SafeActionClientOpts<ServerError, any, any>["handleReturnedServerError"]>;
handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, MetadataSchema, any>["handleServerErrorLog"]>;
handleReturnedServerError: NonNullable<
SafeActionClientOpts<ServerError, MetadataSchema, any>["handleReturnedServerError"]
>;
middlewareFns: MiddlewareFn<ServerError, any, any, any>[];
ctxType: Ctx;
validationStrategy: "typeschema" | "zod";
Expand Down Expand Up @@ -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<MetadataSchema> : 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<MetadataSchema> : undefined,
})
);
}
}
};
Expand Down
17 changes: 9 additions & 8 deletions packages/next-safe-action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SafeActionClientOpts<ServerError, MetadataSchema, ODVES>["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<ServerError, MetadataSchema, ODVES>["handleReturnedServerError"]
>;
const handleReturnedServerError =
createOpts?.handleReturnedServerError ||
((() => DEFAULT_SERVER_ERROR_MESSAGE) as unknown as NonNullable<
SafeActionClientOpts<ServerError, MetadataSchema, ODVES>["handleReturnedServerError"]
>);

return new SafeActionClient({
middlewareFns: [async ({ next }) => next({ ctx: undefined })],
Expand All @@ -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<MetadataSchema> : undefined,
defaultValidationErrorsShape: (createOpts?.defaultValidationErrorsShape ?? "formatted") as ODVES,
throwValidationErrors: Boolean(createOpts?.throwValidationErrors),
Expand Down
22 changes: 20 additions & 2 deletions packages/next-safe-action/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MetadataSchema extends Schema | undefined> = {
clientInput: unknown;
bindArgsClientInputs: unknown[];
ctx: unknown;
metadata: MetadataSchema extends Schema ? Infer<MetadataSchema> : undefined;
};

/**
* Type of options when creating a new safe action client.
*/
Expand All @@ -16,9 +26,17 @@ export type SafeActionClientOpts<
MetadataSchema extends Schema | undefined,
ODVES extends DVES | undefined,
> = {
handleServerErrorLog?: (e: Error) => MaybePromise<void>;
handleReturnedServerError?: (e: Error) => MaybePromise<ServerError>;
defineMetadataSchema?: () => MetadataSchema;
handleReturnedServerError?: (
error: Error,
utils: ServerErrorFunctionUtils<MetadataSchema>
) => MaybePromise<ServerError>;
handleServerErrorLog?: (
originalError: Error,
utils: ServerErrorFunctionUtils<MetadataSchema> & {
returnedError: ServerError;
}
) => MaybePromise<void>;
throwValidationErrors?: boolean;
defaultValidationErrorsShape?: ODVES;
};
Expand Down
8 changes: 5 additions & 3 deletions packages/next-safe-action/src/safe-action-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ export class SafeActionClient<
const CBAVE = undefined,
> {
readonly #validationStrategy: "typeschema" | "zod";
readonly #handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, any, any>["handleServerErrorLog"]>;
readonly #handleServerErrorLog: NonNullable<
SafeActionClientOpts<ServerError, MetadataSchema, ODVES>["handleServerErrorLog"]
>;
readonly #handleReturnedServerError: NonNullable<
SafeActionClientOpts<ServerError, any, any>["handleReturnedServerError"]
SafeActionClientOpts<ServerError, MetadataSchema, ODVES>["handleReturnedServerError"]
>;
readonly #middlewareFns: MiddlewareFn<ServerError, any, any, any>[];
readonly #ctxType = undefined as Ctx;
Expand All @@ -59,7 +61,7 @@ export class SafeActionClient<
ctxType: Ctx;
} & Required<
Pick<
SafeActionClientOpts<ServerError, any, ODVES>,
SafeActionClientOpts<ServerError, MetadataSchema, ODVES>,
"handleReturnedServerError" | "handleServerErrorLog" | "defaultValidationErrorsShape" | "throwValidationErrors"
>
>
Expand Down
20 changes: 11 additions & 9 deletions packages/next-safe-action/src/typeschema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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<SafeActionClientOpts<ServerError, MetadataSchema, ODVES>["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<ServerError, MetadataSchema, ODVES>["handleReturnedServerError"]
>;
const handleReturnedServerError =
createOpts?.handleReturnedServerError ||
((() => DEFAULT_SERVER_ERROR_MESSAGE) as unknown as NonNullable<
SafeActionClientOpts<ServerError, MetadataSchema, ODVES>["handleReturnedServerError"]
>);

return new SafeActionClient({
middlewareFns: [async ({ next }) => next({ ctx: undefined })],
Expand All @@ -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<MetadataSchema> : undefined,
defaultValidationErrorsShape: (createOpts?.defaultValidationErrorsShape ?? "formatted") as ODVES,
throwValidationErrors: Boolean(createOpts?.throwValidationErrors),
Expand Down
19 changes: 13 additions & 6 deletions website/docs/safe-action-client/initialization-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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!";
},
});
Expand Down Expand Up @@ -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:

Expand All @@ -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);
},
});
```
Expand Down
28 changes: 25 additions & 3 deletions website/docs/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<MetadataSchema extends Schema | undefined> = {
clientInput: unknown;
bindArgsClientInputs: unknown[];
ctx: unknown;
metadata: MetadataSchema extends Schema ? Infer<MetadataSchema> : undefined;
};
```

### `SafeActionClientOpts`

Type of options when creating a new safe action client.
Expand All @@ -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<void>;
handleReturnedServerError?: (e: Error) => MaybePromise<ServerError>;
defineMetadataSchema?: () => MetadataSchema;
handleReturnedServerError?: (
error: Error,
utils: ServerErrorFunctionUtils<MetadataSchema>
) => MaybePromise<ServerError>;
handleServerErrorLog?: (
originalError: Error,
utils: ServerErrorFunctionUtils<MetadataSchema> & {
returnedError: ServerError;
}
) => MaybePromise<void>;
throwValidationErrors?: boolean;
defaultValidationErrorsShape?: ODVES;
};
```
Expand Down

0 comments on commit f8fe120

Please sign in to comment.