Skip to content

Commit

Permalink
feat: support setting default validation errors shape per instance (#152
Browse files Browse the repository at this point in the history
)

Code in this PR adds the support for setting a default validation errors shape (formatted or flattened) per-instance. The shape is then overridable in `schema` and `bindArgsSchemas` methods, using `handleValidationErrorsShape` and `handleBindArgsValidationErrorsShape` optional functions.
  • Loading branch information
TheEdoRan committed Jun 5, 2024
1 parent 8e19d94 commit 0565085
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 132 deletions.
3 changes: 2 additions & 1 deletion apps/playground/src/app/(examples)/direct/login-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export const loginUser = action
.schema(schema, {
// Here we use the `flattenValidationErrors` function to customize the returned validation errors
// object to the client.
formatValidationErrors: (ve) => flattenValidationErrors(ve).fieldErrors,
handleValidationErrorsShape: (ve) =>
flattenValidationErrors(ve).fieldErrors,
})
.action(async ({ parsedInput: { username, password } }) => {
if (username === "johndoe") {
Expand Down
18 changes: 9 additions & 9 deletions packages/next-safe-action/src/action-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import { ActionMetadataError, DEFAULT_SERVER_ERROR_MESSAGE, isError, zodValidate
import { ActionServerValidationError, buildValidationErrors } from "./validation-errors";
import type {
BindArgsValidationErrors,
FormatBindArgsValidationErrorsFn,
FormatValidationErrorsFn,
HandleBindArgsValidationErrorsShapeFn,
HandleValidationErrorsShapeFn,
ValidationErrors,
} from "./validation-errors.types";

Expand All @@ -34,12 +34,12 @@ export function actionBuilder<
>(args: {
schema?: S;
bindArgsSchemas?: BAS;
formatValidationErrors: FormatValidationErrorsFn<S, CVE>;
formatBindArgsValidationErrors: FormatBindArgsValidationErrorsFn<BAS, CBAVE>;
handleValidationErrorsShape: HandleValidationErrorsShapeFn<S, CVE>;
handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<BAS, CBAVE>;
metadataSchema: MetadataSchema;
metadata: MD;
handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, any>["handleServerErrorLog"]>;
handleReturnedServerError: NonNullable<SafeActionClientOpts<ServerError, any>["handleReturnedServerError"]>;
handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, any, any>["handleServerErrorLog"]>;
handleReturnedServerError: NonNullable<SafeActionClientOpts<ServerError, any, any>["handleReturnedServerError"]>;
middlewareFns: MiddlewareFn<ServerError, any, any, any>[];
ctxType: Ctx;
validationStrategy: "typeschema" | "zod";
Expand Down Expand Up @@ -161,7 +161,7 @@ export function actionBuilder<
const validationErrors = buildValidationErrors<S>(parsedInput.issues);

middlewareResult.validationErrors = await Promise.resolve(
args.formatValidationErrors(validationErrors)
args.handleValidationErrorsShape(validationErrors)
);
}
}
Expand All @@ -170,7 +170,7 @@ 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.formatBindArgsValidationErrors(bindArgsValidationErrors as BindArgsValidationErrors<BAS>)
args.handleBindArgsValidationErrorsShape(bindArgsValidationErrors as BindArgsValidationErrors<BAS>)
);
}

Expand Down Expand Up @@ -214,7 +214,7 @@ 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<S>;
middlewareResult.validationErrors = await Promise.resolve(args.formatValidationErrors(ve));
middlewareResult.validationErrors = await Promise.resolve(args.handleValidationErrorsShape(ve));
} else {
// If error is not an instance of Error, wrap it in an Error object with
// the default message.
Expand Down
36 changes: 28 additions & 8 deletions packages/next-safe-action/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import type { Infer, Schema } from "@typeschema/main";
import type { SafeActionClientOpts } from "./index.types";
import type { DVES, SafeActionClientOpts } from "./index.types";
import { SafeActionClient } from "./safe-action-client";
import { DEFAULT_SERVER_ERROR_MESSAGE } from "./utils";
import { formatBindArgsValidationErrors, formatValidationErrors } from "./validation-errors";
import {
flattenBindArgsValidationErrors,
flattenValidationErrors,
formatBindArgsValidationErrors,
formatValidationErrors,
} from "./validation-errors";

export { ActionMetadataError, DEFAULT_SERVER_ERROR_MESSAGE } from "./utils";
export { flattenBindArgsValidationErrors, flattenValidationErrors, returnValidationErrors } from "./validation-errors";
export {
flattenBindArgsValidationErrors,
flattenValidationErrors,
formatBindArgsValidationErrors,
formatValidationErrors,
returnValidationErrors,
} from "./validation-errors";

export type * from "./index.types";
export type * from "./validation-errors.types";
Expand All @@ -18,8 +29,12 @@ export type * from "./validation-errors.types";
*
* {@link https://next-safe-action.dev/docs/safe-action-client/initialization-options See docs for more information}
*/
export const createSafeActionClient = <ServerError = string, MetadataSchema extends Schema | undefined = undefined>(
createOpts?: SafeActionClientOpts<ServerError, MetadataSchema>
export const createSafeActionClient = <
ODVES extends DVES | undefined = undefined,
ServerError = string,
MetadataSchema extends Schema | undefined = undefined,
>(
createOpts?: SafeActionClientOpts<ServerError, MetadataSchema, ODVES>
) => {
// If server log function is not provided, default to `console.error` for logging
// server error messages.
Expand All @@ -34,7 +49,7 @@ export const createSafeActionClient = <ServerError = string, MetadataSchema exte
// 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>["handleReturnedServerError"]
SafeActionClientOpts<ServerError, MetadataSchema, ODVES>["handleReturnedServerError"]
>;

return new SafeActionClient({
Expand All @@ -47,7 +62,12 @@ export const createSafeActionClient = <ServerError = string, MetadataSchema exte
ctxType: undefined,
metadataSchema: createOpts?.defineMetadataSchema?.(),
metadata: undefined as MetadataSchema extends Schema ? Infer<MetadataSchema> : undefined,
formatValidationErrorsFn: formatValidationErrors,
formatBindArgsValidationErrorsFn: formatBindArgsValidationErrors,
defaultValidationErrorsShape: (createOpts?.defaultValidationErrorsShape ?? "formatted") as ODVES,
handleValidationErrorsShape:
createOpts?.defaultValidationErrorsShape === "flattened" ? flattenValidationErrors : formatValidationErrors,
handleBindArgsValidationErrorsShape:
createOpts?.defaultValidationErrorsShape === "flattened"
? flattenBindArgsValidationErrors
: formatBindArgsValidationErrors,
});
};
13 changes: 12 additions & 1 deletion packages/next-safe-action/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@ import type { Infer, InferIn, Schema } from "@typeschema/main";
import type { InferArray, InferInArray, MaybePromise, Prettify } from "./utils";
import type { BindArgsValidationErrors, ValidationErrors } from "./validation-errors.types";

/**
* Type of the default validation errors shape passed to `createSafeActionClient` via `defaultValidationErrorsShape`
* property.
*/
export type DVES = "formatted" | "flattened";

/**
* Type of options when creating a new safe action client.
*/
export type SafeActionClientOpts<ServerError, MetadataSchema extends Schema | undefined> = {
export type SafeActionClientOpts<
ServerError,
MetadataSchema extends Schema | undefined,
ODVES extends DVES | undefined,
> = {
handleServerErrorLog?: (e: Error) => MaybePromise<void>;
handleReturnedServerError?: (e: Error) => MaybePromise<ServerError>;
defineMetadataSchema?: () => MetadataSchema;
defaultValidationErrorsShape?: ODVES;
};

/**
Expand Down
101 changes: 62 additions & 39 deletions packages/next-safe-action/src/safe-action-client.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
import type { Infer, Schema } from "@typeschema/main";
import type {} from "zod";
import { actionBuilder } from "./action-builder";
import type { MiddlewareFn, SafeActionClientOpts, ServerCodeFn, StateServerCodeFn } from "./index.types";
import type { DVES, MiddlewareFn, SafeActionClientOpts, ServerCodeFn, StateServerCodeFn } from "./index.types";
import type {
BindArgsValidationErrors,
FormatBindArgsValidationErrorsFn,
FormatValidationErrorsFn,
FlattenedBindArgsValidationErrors,
FlattenedValidationErrors,
HandleBindArgsValidationErrorsShapeFn,
HandleValidationErrorsShapeFn,
ValidationErrors,
} from "./validation-errors.types";

export class SafeActionClient<
ServerError,
ODVES extends DVES | undefined,
MetadataSchema extends Schema | undefined = undefined,
MD = MetadataSchema extends Schema ? Infer<Schema> : undefined,
Ctx = undefined,
S extends Schema | undefined = undefined,
const BAS extends readonly Schema[] = [],
CVE = ValidationErrors<S>,
const CBAVE = BindArgsValidationErrors<BAS>,
CVE = undefined,
const CBAVE = undefined,
> {
readonly #handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, any>["handleServerErrorLog"]>;
readonly #handleReturnedServerError: NonNullable<SafeActionClientOpts<ServerError, any>["handleReturnedServerError"]>;
readonly #validationStrategy: "typeschema" | "zod";

#middlewareFns: MiddlewareFn<ServerError, any, any, any>[];
#ctxType = undefined as Ctx;
#metadataSchema: MetadataSchema;
#metadata: MD;
#schema: S;
#bindArgsSchemas: BAS;
#formatValidationErrorsFn: FormatValidationErrorsFn<S, CVE>;
#formatBindArgsValidationErrorsFn: FormatBindArgsValidationErrorsFn<BAS, CBAVE>;
readonly #handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, any, any>["handleServerErrorLog"]>;
readonly #handleReturnedServerError: NonNullable<
SafeActionClientOpts<ServerError, any, any>["handleReturnedServerError"]
>;
readonly #middlewareFns: MiddlewareFn<ServerError, any, any, any>[];
readonly #ctxType = undefined as Ctx;
readonly #metadataSchema: MetadataSchema;
readonly #metadata: MD;
readonly #schema: S;
readonly #bindArgsSchemas: BAS;
readonly #handleValidationErrorsShape: HandleValidationErrorsShapeFn<S, CVE>;
readonly #handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<BAS, CBAVE>;
readonly #defaultValidationErrorsShape: ODVES;

constructor(
opts: {
Expand All @@ -40,10 +45,15 @@ export class SafeActionClient<
metadata: MD;
schema: S;
bindArgsSchemas: BAS;
formatValidationErrorsFn: FormatValidationErrorsFn<S, CVE>;
formatBindArgsValidationErrorsFn: FormatBindArgsValidationErrorsFn<BAS, CBAVE>;
handleValidationErrorsShape: HandleValidationErrorsShapeFn<S, CVE>;
handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<BAS, CBAVE>;
ctxType: Ctx;
} & Required<Pick<SafeActionClientOpts<ServerError, any>, "handleReturnedServerError" | "handleServerErrorLog">>
} & Required<
Pick<
SafeActionClientOpts<ServerError, any, ODVES>,
"handleReturnedServerError" | "handleServerErrorLog" | "defaultValidationErrorsShape"
>
>
) {
this.#middlewareFns = opts.middlewareFns;
this.#handleServerErrorLog = opts.handleServerErrorLog;
Expand All @@ -53,8 +63,9 @@ export class SafeActionClient<
this.#metadata = opts.metadata;
this.#schema = (opts.schema ?? undefined) as S;
this.#bindArgsSchemas = opts.bindArgsSchemas ?? [];
this.#formatValidationErrorsFn = opts.formatValidationErrorsFn;
this.#formatBindArgsValidationErrorsFn = opts.formatBindArgsValidationErrorsFn;
this.#handleValidationErrorsShape = opts.handleValidationErrorsShape;
this.#handleBindArgsValidationErrorsShape = opts.handleBindArgsValidationErrorsShape;
this.#defaultValidationErrorsShape = opts.defaultValidationErrorsShape;
}

/**
Expand All @@ -73,9 +84,10 @@ export class SafeActionClient<
metadata: this.#metadata,
schema: this.#schema,
bindArgsSchemas: this.#bindArgsSchemas,
formatValidationErrorsFn: this.#formatValidationErrorsFn,
formatBindArgsValidationErrorsFn: this.#formatBindArgsValidationErrorsFn,
handleValidationErrorsShape: this.#handleValidationErrorsShape,
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
ctxType: undefined as NextCtx,
defaultValidationErrorsShape: this.#defaultValidationErrorsShape,
});
}

Expand All @@ -95,9 +107,10 @@ export class SafeActionClient<
metadata: data,
schema: this.#schema,
bindArgsSchemas: this.#bindArgsSchemas,
formatValidationErrorsFn: this.#formatValidationErrorsFn,
formatBindArgsValidationErrorsFn: this.#formatBindArgsValidationErrorsFn,
handleValidationErrorsShape: this.#handleValidationErrorsShape,
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
ctxType: undefined as Ctx,
defaultValidationErrorsShape: this.#defaultValidationErrorsShape,
});
}

Expand All @@ -108,10 +121,13 @@ export class SafeActionClient<
*
* {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#schema See docs for more information}
*/
schema<OS extends Schema, OCVE = ValidationErrors<OS>>(
schema<
OS extends Schema,
OCVE = ODVES extends "flattened" ? FlattenedValidationErrors<ValidationErrors<OS>> : ValidationErrors<OS>,
>(
schema: OS,
utils?: {
formatValidationErrors?: FormatValidationErrorsFn<OS, OCVE>;
handleValidationErrorsShape?: HandleValidationErrorsShapeFn<OS, OCVE>;
}
) {
return new SafeActionClient({
Expand All @@ -123,10 +139,11 @@ export class SafeActionClient<
metadata: this.#metadata,
schema,
bindArgsSchemas: this.#bindArgsSchemas,
formatValidationErrorsFn: (utils?.formatValidationErrors ??
this.#formatValidationErrorsFn) as FormatValidationErrorsFn<OS, OCVE>,
formatBindArgsValidationErrorsFn: this.#formatBindArgsValidationErrorsFn,
handleValidationErrorsShape: (utils?.handleValidationErrorsShape ??
this.#handleValidationErrorsShape) as HandleValidationErrorsShapeFn<OS, OCVE>,
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
ctxType: undefined as Ctx,
defaultValidationErrorsShape: this.#defaultValidationErrorsShape,
});
}

Expand All @@ -137,9 +154,14 @@ export class SafeActionClient<
*
* {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#schema See docs for more information}
*/
bindArgsSchemas<const OBAS extends readonly Schema[], OCBAVE = BindArgsValidationErrors<OBAS>>(
bindArgsSchemas<
const OBAS extends readonly Schema[],
OCBAVE = ODVES extends "flattened"
? FlattenedBindArgsValidationErrors<BindArgsValidationErrors<OBAS>>
: BindArgsValidationErrors<OBAS>,
>(
bindArgsSchemas: OBAS,
utils?: { formatBindArgsValidationErrors?: FormatBindArgsValidationErrorsFn<OBAS, OCBAVE> }
utils?: { handleBindArgsValidationErrorsShape?: HandleBindArgsValidationErrorsShapeFn<OBAS, OCBAVE> }
) {
return new SafeActionClient({
middlewareFns: this.#middlewareFns,
Expand All @@ -150,10 +172,11 @@ export class SafeActionClient<
metadata: this.#metadata,
schema: this.#schema,
bindArgsSchemas,
formatValidationErrorsFn: this.#formatValidationErrorsFn,
formatBindArgsValidationErrorsFn: (utils?.formatBindArgsValidationErrors ??
this.#formatBindArgsValidationErrorsFn) as FormatBindArgsValidationErrorsFn<OBAS, OCBAVE>,
handleValidationErrorsShape: this.#handleValidationErrorsShape,
handleBindArgsValidationErrorsShape: (utils?.handleBindArgsValidationErrorsShape ??
this.#handleBindArgsValidationErrorsShape) as HandleBindArgsValidationErrorsShapeFn<OBAS, OCBAVE>,
ctxType: undefined as Ctx,
defaultValidationErrorsShape: this.#defaultValidationErrorsShape,
});
}

Expand All @@ -174,8 +197,8 @@ export class SafeActionClient<
metadata: this.#metadata,
schema: this.#schema,
bindArgsSchemas: this.#bindArgsSchemas,
formatValidationErrors: this.#formatValidationErrorsFn,
formatBindArgsValidationErrors: this.#formatBindArgsValidationErrorsFn,
handleValidationErrorsShape: this.#handleValidationErrorsShape,
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
}).action(serverCodeFn);
}

Expand All @@ -197,8 +220,8 @@ export class SafeActionClient<
metadata: this.#metadata,
schema: this.#schema,
bindArgsSchemas: this.#bindArgsSchemas,
formatValidationErrors: this.#formatValidationErrorsFn,
formatBindArgsValidationErrors: this.#formatBindArgsValidationErrorsFn,
handleValidationErrorsShape: this.#handleValidationErrorsShape,
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
}).stateAction(serverCodeFn);
}
}
Loading

0 comments on commit 0565085

Please sign in to comment.