Skip to content

Commit

Permalink
refactor: pass utils to validation errors shape customizer functions (#…
Browse files Browse the repository at this point in the history
…263)

Code in this PR adds a second argument to `handleValidationErrorsShape`
and `handleBindArgsValidationErrorsShape` functions, called `utils`,
which is an object that contains `clientInput`, `bindArgsClientInputs`,
`metadata` and `ctx` properties. This addition allows you to set dynamic
validation errors based on current action execution data.

re #256
  • Loading branch information
TheEdoRan authored Sep 8, 2024
1 parent 3d32f9d commit a789d0a
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 23 deletions.
36 changes: 31 additions & 5 deletions packages/next-safe-action/src/action-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export function actionBuilder<
bindArgsSchemas?: BAS;
outputSchema?: OS;
validationAdapter: ValidationAdapter;
handleValidationErrorsShape: HandleValidationErrorsShapeFn<IS, CVE>;
handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<BAS, CBAVE>;
handleValidationErrorsShape: HandleValidationErrorsShapeFn<IS, BAS, MD, Ctx, CVE>;
handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<IS, BAS, MD, Ctx, CBAVE>;
metadataSchema: MetadataSchema;
metadata: MD;
handleServerError: NonNullable<SafeActionClientOpts<ServerError, MetadataSchema, any>["handleServerError"]>;
Expand Down Expand Up @@ -179,7 +179,14 @@ export function actionBuilder<
const validationErrors = buildValidationErrors<IS>(parsedInput.issues);

middlewareResult.validationErrors = await Promise.resolve(
args.handleValidationErrorsShape(validationErrors)
args.handleValidationErrorsShape(validationErrors, {
clientInput: clientInputs.at(-1) as IS extends Schema ? InferIn<IS> : undefined,
bindArgsClientInputs: (bindArgsSchemas.length
? clientInputs.slice(0, -1)
: []) as InferInArray<BAS>,
ctx: currentCtx as Ctx,
metadata: args.metadata as MetadataSchema extends Schema ? Infer<MetadataSchema> : undefined,
})
);
}
}
Expand All @@ -188,7 +195,17 @@ export function actionBuilder<
// If there are bind args validation errors, format them and store them in the middleware result.
if (hasBindValidationErrors) {
middlewareResult.bindArgsValidationErrors = await Promise.resolve(
args.handleBindArgsValidationErrorsShape(bindArgsValidationErrors as BindArgsValidationErrors<BAS>)
args.handleBindArgsValidationErrorsShape(
bindArgsValidationErrors as BindArgsValidationErrors<BAS>,
{
clientInput: clientInputs.at(-1) as IS extends Schema ? InferIn<IS> : undefined,
bindArgsClientInputs: (bindArgsSchemas.length
? clientInputs.slice(0, -1)
: []) as InferInArray<BAS>,
ctx: currentCtx as Ctx,
metadata: args.metadata as MetadataSchema extends Schema ? Infer<MetadataSchema> : undefined,
}
)
);
}

Expand Down Expand Up @@ -241,7 +258,16 @@ export function actionBuilder<
// If error is `ActionServerValidationError`, return `validationErrors` as if schema validation would fail.
if (e instanceof ActionServerValidationError) {
const ve = e.validationErrors as ValidationErrors<IS>;
middlewareResult.validationErrors = await Promise.resolve(args.handleValidationErrorsShape(ve));
middlewareResult.validationErrors = await Promise.resolve(
args.handleValidationErrorsShape(ve, {
clientInput: clientInputs.at(-1) as IS extends Schema ? InferIn<IS> : undefined,
bindArgsClientInputs: (bindArgsSchemas.length
? clientInputs.slice(0, -1)
: []) as InferInArray<BAS>,
ctx: currentCtx as Ctx,
metadata: args.metadata as MetadataSchema extends Schema ? Infer<MetadataSchema> : undefined,
})
);
} else {
// If error is not an instance of Error, wrap it in an Error object with
// the default message.
Expand Down
27 changes: 17 additions & 10 deletions packages/next-safe-action/src/safe-action-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export class SafeActionClient<
readonly #ctxType: Ctx;
readonly #bindArgsSchemas: BAS;
readonly #validationAdapter: ValidationAdapter;
readonly #handleValidationErrorsShape: HandleValidationErrorsShapeFn<IS, CVE>;
readonly #handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<BAS, CBAVE>;
readonly #handleValidationErrorsShape: HandleValidationErrorsShapeFn<IS, BAS, MD, Ctx, CVE>;
readonly #handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<IS, BAS, MD, Ctx, CBAVE>;
readonly #defaultValidationErrorsShape: ODVES;
readonly #throwValidationErrors: boolean;

Expand All @@ -56,8 +56,8 @@ export class SafeActionClient<
outputSchema: OS;
bindArgsSchemas: BAS;
validationAdapter: ValidationAdapter;
handleValidationErrorsShape: HandleValidationErrorsShapeFn<IS, CVE>;
handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<BAS, CBAVE>;
handleValidationErrorsShape: HandleValidationErrorsShapeFn<IS, BAS, MD, Ctx, CVE>;
handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<IS, BAS, MD, Ctx, CBAVE>;
ctxType: Ctx;
} & Required<
Pick<
Expand Down Expand Up @@ -143,7 +143,7 @@ export class SafeActionClient<
>(
inputSchema: OIS,
utils?: {
handleValidationErrorsShape?: HandleValidationErrorsShapeFn<AIS, OCVE>;
handleValidationErrorsShape?: HandleValidationErrorsShapeFn<AIS, BAS, MD, Ctx, OCVE>;
}
) {
return new SafeActionClient({
Expand All @@ -163,8 +163,9 @@ export class SafeActionClient<
outputSchema: this.#outputSchema,
validationAdapter: this.#validationAdapter,
handleValidationErrorsShape: (utils?.handleValidationErrorsShape ??
this.#handleValidationErrorsShape) as HandleValidationErrorsShapeFn<AIS, OCVE>,
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
this.#handleValidationErrorsShape) as HandleValidationErrorsShapeFn<AIS, BAS, MD, Ctx, OCVE>,
handleBindArgsValidationErrorsShape: this
.#handleBindArgsValidationErrorsShape as HandleBindArgsValidationErrorsShapeFn<AIS, BAS, MD, Ctx, CBAVE>,
ctxType: {} as Ctx,
defaultValidationErrorsShape: this.#defaultValidationErrorsShape,
throwValidationErrors: this.#throwValidationErrors,
Expand All @@ -185,7 +186,7 @@ export class SafeActionClient<
: BindArgsValidationErrors<OBAS>,
>(
bindArgsSchemas: OBAS,
utils?: { handleBindArgsValidationErrorsShape?: HandleBindArgsValidationErrorsShapeFn<OBAS, OCBAVE> }
utils?: { handleBindArgsValidationErrorsShape?: HandleBindArgsValidationErrorsShapeFn<IS, OBAS, MD, Ctx, OCBAVE> }
) {
return new SafeActionClient({
middlewareFns: this.#middlewareFns,
Expand All @@ -196,9 +197,15 @@ export class SafeActionClient<
bindArgsSchemas,
outputSchema: this.#outputSchema,
validationAdapter: this.#validationAdapter,
handleValidationErrorsShape: this.#handleValidationErrorsShape,
handleValidationErrorsShape: this.#handleValidationErrorsShape as unknown as HandleValidationErrorsShapeFn<
IS,
OBAS,
MD,
Ctx,
CVE
>,
handleBindArgsValidationErrorsShape: (utils?.handleBindArgsValidationErrorsShape ??
this.#handleBindArgsValidationErrorsShape) as HandleBindArgsValidationErrorsShapeFn<OBAS, OCBAVE>,
this.#handleBindArgsValidationErrorsShape) as HandleBindArgsValidationErrorsShapeFn<IS, OBAS, MD, Ctx, OCBAVE>,
ctxType: {} as Ctx,
defaultValidationErrorsShape: this.#defaultValidationErrorsShape,
throwValidationErrors: this.#throwValidationErrors,
Expand Down
34 changes: 29 additions & 5 deletions packages/next-safe-action/src/validation-errors.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Infer, Schema } from "./adapters/types";
import type { Infer, InferIn, Schema } from "./adapters/types";
import type { Prettify } from "./utils.types";

// Object with an optional list of validation errors.
Expand Down Expand Up @@ -46,13 +46,37 @@ export type FlattenedBindArgsValidationErrors<BAVE extends readonly ValidationEr
/**
* Type of the function used to format validation errors.
*/
export type HandleValidationErrorsShapeFn<S extends Schema | undefined, CVE> = (
validationErrors: ValidationErrors<S>
export type HandleValidationErrorsShapeFn<
S extends Schema | undefined,
BAS extends readonly Schema[],
MD,
Ctx extends object,
CVE,
> = (
validationErrors: ValidationErrors<S>,
utils: {
clientInput: S extends Schema ? InferIn<S> : undefined;
bindArgsClientInputs: BAS;
metadata: MD;
ctx: Prettify<Ctx>;
}
) => CVE;

/**
* Type of the function used to format bind arguments validation errors.
*/
export type HandleBindArgsValidationErrorsShapeFn<BAS extends readonly Schema[], CBAVE> = (
bindArgsValidationErrors: BindArgsValidationErrors<BAS>
export type HandleBindArgsValidationErrorsShapeFn<
S extends Schema | undefined,
BAS extends readonly Schema[],
MD,
Ctx extends object,
CBAVE,
> = (
bindArgsValidationErrors: BindArgsValidationErrors<BAS>,
utils: {
clientInput: S extends Schema ? InferIn<S> : undefined;
bindArgsClientInputs: BAS;
metadata: MD;
ctx: Prettify<Ctx>;
}
) => CBAVE;
8 changes: 5 additions & 3 deletions website/docs/define-actions/validation-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This can be customized both at the safe action client level and at the action le
- using [`defaultValidationErrorsShape`](/docs/define-actions/create-the-client#defaultvalidationerrorsshape) optional property in `createSafeActionClient`;
- using `handleValidationErrorsShape` and `handleBindArgsValidationErrorsShape` optional functions in [`schema`](/docs/define-actions/instance-methods#schema) and [`bindArgsSchemas`](/docs/define-actions/instance-methods#bindargsschemas) methods.

The second way overrides the shape set at the instance level, per action. More information below.
The second way overrides the shape set at the instance level, per action.

For example, if you want to flatten the validation errors (emulation of Zod's [`flatten`](https://zod.dev/ERROR_HANDLING?id=flattening-errors) method), you can (but not required to) use the `flattenValidationErrors` utility function exported from the library, combining it with `handleValidationErrorsShape` inside `schema` method:

Expand All @@ -39,19 +39,21 @@ export const loginUser = actionClient
// Here we use the `flattenValidationErrors` function to customize the returned validation errors
// object to the client.
// highlight-next-line
handleValidationErrorsShape: (ve) => flattenValidationErrors(ve).fieldErrors,
handleValidationErrorsShape: (ve, utils) => flattenValidationErrors(ve).fieldErrors,
})
.bindArgsSchemas(bindArgsSchemas, {
// Here we use the `flattenBindArgsValidatonErrors` function to customize the returned bind args
// validation errors object array to the client.
// highlight-next-line
handleBindArgsValidationErrors: (ve) => flattenBindArgsValidationErrors(ve),
handleBindArgsValidationErrors: (ve, utils) => flattenBindArgsValidationErrors(ve),
})
.action(async ({ parsedInput: { username, password } }) => {
// Your code here...
});
```

The second argument of both `handleValidationErrorsShape` and `handleBindArgsValidationErrors` functions is an `utils` object that contains info about the current action execution (`clientInput`, `bindArgsClientInputs`, `metadata` and `ctx` properties). It's passed to the functions to allow granular and dynamic customization of the validation errors shape.

:::note
If you chain multiple `schema` methods, as explained in the [Extend previous schema](/docs/define-actions/extend-previous-schemas) page, and want to override the default validation errors shape, you **must** use `handleValidationErrorsShape` inside the last `schema` method, otherwise there would be a type mismatch in the returned action result.
:::
Expand Down

0 comments on commit a789d0a

Please sign in to comment.