Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: TheEdoRan/next-safe-action
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v7.9.1
Choose a base ref
...
head repository: TheEdoRan/next-safe-action
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v7.9.2
Choose a head ref
  • 1 commit
  • 4 files changed
  • 1 contributor

Commits on Sep 8, 2024

  1. refactor: pass utils to validation errors shape customizer functions (#…

    …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
    TheEdoRan authored Sep 8, 2024
    Copy the full SHA
    a789d0a View commit details
36 changes: 31 additions & 5 deletions packages/next-safe-action/src/action-builder.ts
Original file line number Diff line number Diff line change
@@ -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"]>;
@@ -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,
})
);
}
}
@@ -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,
}
)
);
}

@@ -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.
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
@@ -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;

@@ -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<
@@ -143,7 +143,7 @@ export class SafeActionClient<
>(
inputSchema: OIS,
utils?: {
handleValidationErrorsShape?: HandleValidationErrorsShapeFn<AIS, OCVE>;
handleValidationErrorsShape?: HandleValidationErrorsShapeFn<AIS, BAS, MD, Ctx, OCVE>;
}
) {
return new SafeActionClient({
@@ -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,
@@ -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,
@@ -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,
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.
@@ -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
@@ -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:

@@ -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.
:::