Skip to content

Commit 0565085

Browse files
authoredJun 5, 2024··
feat: support setting default validation errors shape per instance (#152)
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.
1 parent 8e19d94 commit 0565085

File tree

12 files changed

+195
-132
lines changed

12 files changed

+195
-132
lines changed
 

‎apps/playground/src/app/(examples)/direct/login-action.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export const loginUser = action
1717
.schema(schema, {
1818
// Here we use the `flattenValidationErrors` function to customize the returned validation errors
1919
// object to the client.
20-
formatValidationErrors: (ve) => flattenValidationErrors(ve).fieldErrors,
20+
handleValidationErrorsShape: (ve) =>
21+
flattenValidationErrors(ve).fieldErrors,
2122
})
2223
.action(async ({ parsedInput: { username, password } }) => {
2324
if (username === "johndoe") {

‎packages/next-safe-action/src/action-builder.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import { ActionMetadataError, DEFAULT_SERVER_ERROR_MESSAGE, isError, zodValidate
1717
import { ActionServerValidationError, buildValidationErrors } from "./validation-errors";
1818
import type {
1919
BindArgsValidationErrors,
20-
FormatBindArgsValidationErrorsFn,
21-
FormatValidationErrorsFn,
20+
HandleBindArgsValidationErrorsShapeFn,
21+
HandleValidationErrorsShapeFn,
2222
ValidationErrors,
2323
} from "./validation-errors.types";
2424

@@ -34,12 +34,12 @@ export function actionBuilder<
3434
>(args: {
3535
schema?: S;
3636
bindArgsSchemas?: BAS;
37-
formatValidationErrors: FormatValidationErrorsFn<S, CVE>;
38-
formatBindArgsValidationErrors: FormatBindArgsValidationErrorsFn<BAS, CBAVE>;
37+
handleValidationErrorsShape: HandleValidationErrorsShapeFn<S, CVE>;
38+
handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<BAS, CBAVE>;
3939
metadataSchema: MetadataSchema;
4040
metadata: MD;
41-
handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, any>["handleServerErrorLog"]>;
42-
handleReturnedServerError: NonNullable<SafeActionClientOpts<ServerError, any>["handleReturnedServerError"]>;
41+
handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, any, any>["handleServerErrorLog"]>;
42+
handleReturnedServerError: NonNullable<SafeActionClientOpts<ServerError, any, any>["handleReturnedServerError"]>;
4343
middlewareFns: MiddlewareFn<ServerError, any, any, any>[];
4444
ctxType: Ctx;
4545
validationStrategy: "typeschema" | "zod";
@@ -161,7 +161,7 @@ export function actionBuilder<
161161
const validationErrors = buildValidationErrors<S>(parsedInput.issues);
162162

163163
middlewareResult.validationErrors = await Promise.resolve(
164-
args.formatValidationErrors(validationErrors)
164+
args.handleValidationErrorsShape(validationErrors)
165165
);
166166
}
167167
}
@@ -170,7 +170,7 @@ export function actionBuilder<
170170
// If there are bind args validation errors, format them and store them in the middleware result.
171171
if (hasBindValidationErrors) {
172172
middlewareResult.bindArgsValidationErrors = await Promise.resolve(
173-
args.formatBindArgsValidationErrors(bindArgsValidationErrors as BindArgsValidationErrors<BAS>)
173+
args.handleBindArgsValidationErrorsShape(bindArgsValidationErrors as BindArgsValidationErrors<BAS>)
174174
);
175175
}
176176

@@ -214,7 +214,7 @@ export function actionBuilder<
214214
// If error is `ActionServerValidationError`, return `validationErrors` as if schema validation would fail.
215215
if (e instanceof ActionServerValidationError) {
216216
const ve = e.validationErrors as ValidationErrors<S>;
217-
middlewareResult.validationErrors = await Promise.resolve(args.formatValidationErrors(ve));
217+
middlewareResult.validationErrors = await Promise.resolve(args.handleValidationErrorsShape(ve));
218218
} else {
219219
// If error is not an instance of Error, wrap it in an Error object with
220220
// the default message.

‎packages/next-safe-action/src/index.ts

+28-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import type { Infer, Schema } from "@typeschema/main";
2-
import type { SafeActionClientOpts } from "./index.types";
2+
import type { DVES, SafeActionClientOpts } from "./index.types";
33
import { SafeActionClient } from "./safe-action-client";
44
import { DEFAULT_SERVER_ERROR_MESSAGE } from "./utils";
5-
import { formatBindArgsValidationErrors, formatValidationErrors } from "./validation-errors";
5+
import {
6+
flattenBindArgsValidationErrors,
7+
flattenValidationErrors,
8+
formatBindArgsValidationErrors,
9+
formatValidationErrors,
10+
} from "./validation-errors";
611

712
export { ActionMetadataError, DEFAULT_SERVER_ERROR_MESSAGE } from "./utils";
8-
export { flattenBindArgsValidationErrors, flattenValidationErrors, returnValidationErrors } from "./validation-errors";
13+
export {
14+
flattenBindArgsValidationErrors,
15+
flattenValidationErrors,
16+
formatBindArgsValidationErrors,
17+
formatValidationErrors,
18+
returnValidationErrors,
19+
} from "./validation-errors";
920

1021
export type * from "./index.types";
1122
export type * from "./validation-errors.types";
@@ -18,8 +29,12 @@ export type * from "./validation-errors.types";
1829
*
1930
* {@link https://next-safe-action.dev/docs/safe-action-client/initialization-options See docs for more information}
2031
*/
21-
export const createSafeActionClient = <ServerError = string, MetadataSchema extends Schema | undefined = undefined>(
22-
createOpts?: SafeActionClientOpts<ServerError, MetadataSchema>
32+
export const createSafeActionClient = <
33+
ODVES extends DVES | undefined = undefined,
34+
ServerError = string,
35+
MetadataSchema extends Schema | undefined = undefined,
36+
>(
37+
createOpts?: SafeActionClientOpts<ServerError, MetadataSchema, ODVES>
2338
) => {
2439
// If server log function is not provided, default to `console.error` for logging
2540
// server error messages.
@@ -34,7 +49,7 @@ export const createSafeActionClient = <ServerError = string, MetadataSchema exte
3449
// Otherwise mask the error and use a generic message.
3550
const handleReturnedServerError = ((e: Error) =>
3651
createOpts?.handleReturnedServerError?.(e) || DEFAULT_SERVER_ERROR_MESSAGE) as NonNullable<
37-
SafeActionClientOpts<ServerError, MetadataSchema>["handleReturnedServerError"]
52+
SafeActionClientOpts<ServerError, MetadataSchema, ODVES>["handleReturnedServerError"]
3853
>;
3954

4055
return new SafeActionClient({
@@ -47,7 +62,12 @@ export const createSafeActionClient = <ServerError = string, MetadataSchema exte
4762
ctxType: undefined,
4863
metadataSchema: createOpts?.defineMetadataSchema?.(),
4964
metadata: undefined as MetadataSchema extends Schema ? Infer<MetadataSchema> : undefined,
50-
formatValidationErrorsFn: formatValidationErrors,
51-
formatBindArgsValidationErrorsFn: formatBindArgsValidationErrors,
65+
defaultValidationErrorsShape: (createOpts?.defaultValidationErrorsShape ?? "formatted") as ODVES,
66+
handleValidationErrorsShape:
67+
createOpts?.defaultValidationErrorsShape === "flattened" ? flattenValidationErrors : formatValidationErrors,
68+
handleBindArgsValidationErrorsShape:
69+
createOpts?.defaultValidationErrorsShape === "flattened"
70+
? flattenBindArgsValidationErrors
71+
: formatBindArgsValidationErrors,
5272
});
5373
};

‎packages/next-safe-action/src/index.types.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,24 @@ import type { Infer, InferIn, Schema } from "@typeschema/main";
22
import type { InferArray, InferInArray, MaybePromise, Prettify } from "./utils";
33
import type { BindArgsValidationErrors, ValidationErrors } from "./validation-errors.types";
44

5+
/**
6+
* Type of the default validation errors shape passed to `createSafeActionClient` via `defaultValidationErrorsShape`
7+
* property.
8+
*/
9+
export type DVES = "formatted" | "flattened";
10+
511
/**
612
* Type of options when creating a new safe action client.
713
*/
8-
export type SafeActionClientOpts<ServerError, MetadataSchema extends Schema | undefined> = {
14+
export type SafeActionClientOpts<
15+
ServerError,
16+
MetadataSchema extends Schema | undefined,
17+
ODVES extends DVES | undefined,
18+
> = {
919
handleServerErrorLog?: (e: Error) => MaybePromise<void>;
1020
handleReturnedServerError?: (e: Error) => MaybePromise<ServerError>;
1121
defineMetadataSchema?: () => MetadataSchema;
22+
defaultValidationErrorsShape?: ODVES;
1223
};
1324

1425
/**

‎packages/next-safe-action/src/safe-action-client.ts

+62-39
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,41 @@
11
import type { Infer, Schema } from "@typeschema/main";
22
import type {} from "zod";
33
import { actionBuilder } from "./action-builder";
4-
import type { MiddlewareFn, SafeActionClientOpts, ServerCodeFn, StateServerCodeFn } from "./index.types";
4+
import type { DVES, MiddlewareFn, SafeActionClientOpts, ServerCodeFn, StateServerCodeFn } from "./index.types";
55
import type {
66
BindArgsValidationErrors,
7-
FormatBindArgsValidationErrorsFn,
8-
FormatValidationErrorsFn,
7+
FlattenedBindArgsValidationErrors,
8+
FlattenedValidationErrors,
9+
HandleBindArgsValidationErrorsShapeFn,
10+
HandleValidationErrorsShapeFn,
911
ValidationErrors,
1012
} from "./validation-errors.types";
1113

1214
export class SafeActionClient<
1315
ServerError,
16+
ODVES extends DVES | undefined,
1417
MetadataSchema extends Schema | undefined = undefined,
1518
MD = MetadataSchema extends Schema ? Infer<Schema> : undefined,
1619
Ctx = undefined,
1720
S extends Schema | undefined = undefined,
1821
const BAS extends readonly Schema[] = [],
19-
CVE = ValidationErrors<S>,
20-
const CBAVE = BindArgsValidationErrors<BAS>,
22+
CVE = undefined,
23+
const CBAVE = undefined,
2124
> {
22-
readonly #handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, any>["handleServerErrorLog"]>;
23-
readonly #handleReturnedServerError: NonNullable<SafeActionClientOpts<ServerError, any>["handleReturnedServerError"]>;
2425
readonly #validationStrategy: "typeschema" | "zod";
25-
26-
#middlewareFns: MiddlewareFn<ServerError, any, any, any>[];
27-
#ctxType = undefined as Ctx;
28-
#metadataSchema: MetadataSchema;
29-
#metadata: MD;
30-
#schema: S;
31-
#bindArgsSchemas: BAS;
32-
#formatValidationErrorsFn: FormatValidationErrorsFn<S, CVE>;
33-
#formatBindArgsValidationErrorsFn: FormatBindArgsValidationErrorsFn<BAS, CBAVE>;
26+
readonly #handleServerErrorLog: NonNullable<SafeActionClientOpts<ServerError, any, any>["handleServerErrorLog"]>;
27+
readonly #handleReturnedServerError: NonNullable<
28+
SafeActionClientOpts<ServerError, any, any>["handleReturnedServerError"]
29+
>;
30+
readonly #middlewareFns: MiddlewareFn<ServerError, any, any, any>[];
31+
readonly #ctxType = undefined as Ctx;
32+
readonly #metadataSchema: MetadataSchema;
33+
readonly #metadata: MD;
34+
readonly #schema: S;
35+
readonly #bindArgsSchemas: BAS;
36+
readonly #handleValidationErrorsShape: HandleValidationErrorsShapeFn<S, CVE>;
37+
readonly #handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<BAS, CBAVE>;
38+
readonly #defaultValidationErrorsShape: ODVES;
3439

3540
constructor(
3641
opts: {
@@ -40,10 +45,15 @@ export class SafeActionClient<
4045
metadata: MD;
4146
schema: S;
4247
bindArgsSchemas: BAS;
43-
formatValidationErrorsFn: FormatValidationErrorsFn<S, CVE>;
44-
formatBindArgsValidationErrorsFn: FormatBindArgsValidationErrorsFn<BAS, CBAVE>;
48+
handleValidationErrorsShape: HandleValidationErrorsShapeFn<S, CVE>;
49+
handleBindArgsValidationErrorsShape: HandleBindArgsValidationErrorsShapeFn<BAS, CBAVE>;
4550
ctxType: Ctx;
46-
} & Required<Pick<SafeActionClientOpts<ServerError, any>, "handleReturnedServerError" | "handleServerErrorLog">>
51+
} & Required<
52+
Pick<
53+
SafeActionClientOpts<ServerError, any, ODVES>,
54+
"handleReturnedServerError" | "handleServerErrorLog" | "defaultValidationErrorsShape"
55+
>
56+
>
4757
) {
4858
this.#middlewareFns = opts.middlewareFns;
4959
this.#handleServerErrorLog = opts.handleServerErrorLog;
@@ -53,8 +63,9 @@ export class SafeActionClient<
5363
this.#metadata = opts.metadata;
5464
this.#schema = (opts.schema ?? undefined) as S;
5565
this.#bindArgsSchemas = opts.bindArgsSchemas ?? [];
56-
this.#formatValidationErrorsFn = opts.formatValidationErrorsFn;
57-
this.#formatBindArgsValidationErrorsFn = opts.formatBindArgsValidationErrorsFn;
66+
this.#handleValidationErrorsShape = opts.handleValidationErrorsShape;
67+
this.#handleBindArgsValidationErrorsShape = opts.handleBindArgsValidationErrorsShape;
68+
this.#defaultValidationErrorsShape = opts.defaultValidationErrorsShape;
5869
}
5970

6071
/**
@@ -73,9 +84,10 @@ export class SafeActionClient<
7384
metadata: this.#metadata,
7485
schema: this.#schema,
7586
bindArgsSchemas: this.#bindArgsSchemas,
76-
formatValidationErrorsFn: this.#formatValidationErrorsFn,
77-
formatBindArgsValidationErrorsFn: this.#formatBindArgsValidationErrorsFn,
87+
handleValidationErrorsShape: this.#handleValidationErrorsShape,
88+
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
7889
ctxType: undefined as NextCtx,
90+
defaultValidationErrorsShape: this.#defaultValidationErrorsShape,
7991
});
8092
}
8193

@@ -95,9 +107,10 @@ export class SafeActionClient<
95107
metadata: data,
96108
schema: this.#schema,
97109
bindArgsSchemas: this.#bindArgsSchemas,
98-
formatValidationErrorsFn: this.#formatValidationErrorsFn,
99-
formatBindArgsValidationErrorsFn: this.#formatBindArgsValidationErrorsFn,
110+
handleValidationErrorsShape: this.#handleValidationErrorsShape,
111+
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
100112
ctxType: undefined as Ctx,
113+
defaultValidationErrorsShape: this.#defaultValidationErrorsShape,
101114
});
102115
}
103116

@@ -108,10 +121,13 @@ export class SafeActionClient<
108121
*
109122
* {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#schema See docs for more information}
110123
*/
111-
schema<OS extends Schema, OCVE = ValidationErrors<OS>>(
124+
schema<
125+
OS extends Schema,
126+
OCVE = ODVES extends "flattened" ? FlattenedValidationErrors<ValidationErrors<OS>> : ValidationErrors<OS>,
127+
>(
112128
schema: OS,
113129
utils?: {
114-
formatValidationErrors?: FormatValidationErrorsFn<OS, OCVE>;
130+
handleValidationErrorsShape?: HandleValidationErrorsShapeFn<OS, OCVE>;
115131
}
116132
) {
117133
return new SafeActionClient({
@@ -123,10 +139,11 @@ export class SafeActionClient<
123139
metadata: this.#metadata,
124140
schema,
125141
bindArgsSchemas: this.#bindArgsSchemas,
126-
formatValidationErrorsFn: (utils?.formatValidationErrors ??
127-
this.#formatValidationErrorsFn) as FormatValidationErrorsFn<OS, OCVE>,
128-
formatBindArgsValidationErrorsFn: this.#formatBindArgsValidationErrorsFn,
142+
handleValidationErrorsShape: (utils?.handleValidationErrorsShape ??
143+
this.#handleValidationErrorsShape) as HandleValidationErrorsShapeFn<OS, OCVE>,
144+
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
129145
ctxType: undefined as Ctx,
146+
defaultValidationErrorsShape: this.#defaultValidationErrorsShape,
130147
});
131148
}
132149

@@ -137,9 +154,14 @@ export class SafeActionClient<
137154
*
138155
* {@link https://next-safe-action.dev/docs/safe-action-client/instance-methods#schema See docs for more information}
139156
*/
140-
bindArgsSchemas<const OBAS extends readonly Schema[], OCBAVE = BindArgsValidationErrors<OBAS>>(
157+
bindArgsSchemas<
158+
const OBAS extends readonly Schema[],
159+
OCBAVE = ODVES extends "flattened"
160+
? FlattenedBindArgsValidationErrors<BindArgsValidationErrors<OBAS>>
161+
: BindArgsValidationErrors<OBAS>,
162+
>(
141163
bindArgsSchemas: OBAS,
142-
utils?: { formatBindArgsValidationErrors?: FormatBindArgsValidationErrorsFn<OBAS, OCBAVE> }
164+
utils?: { handleBindArgsValidationErrorsShape?: HandleBindArgsValidationErrorsShapeFn<OBAS, OCBAVE> }
143165
) {
144166
return new SafeActionClient({
145167
middlewareFns: this.#middlewareFns,
@@ -150,10 +172,11 @@ export class SafeActionClient<
150172
metadata: this.#metadata,
151173
schema: this.#schema,
152174
bindArgsSchemas,
153-
formatValidationErrorsFn: this.#formatValidationErrorsFn,
154-
formatBindArgsValidationErrorsFn: (utils?.formatBindArgsValidationErrors ??
155-
this.#formatBindArgsValidationErrorsFn) as FormatBindArgsValidationErrorsFn<OBAS, OCBAVE>,
175+
handleValidationErrorsShape: this.#handleValidationErrorsShape,
176+
handleBindArgsValidationErrorsShape: (utils?.handleBindArgsValidationErrorsShape ??
177+
this.#handleBindArgsValidationErrorsShape) as HandleBindArgsValidationErrorsShapeFn<OBAS, OCBAVE>,
156178
ctxType: undefined as Ctx,
179+
defaultValidationErrorsShape: this.#defaultValidationErrorsShape,
157180
});
158181
}
159182

@@ -174,8 +197,8 @@ export class SafeActionClient<
174197
metadata: this.#metadata,
175198
schema: this.#schema,
176199
bindArgsSchemas: this.#bindArgsSchemas,
177-
formatValidationErrors: this.#formatValidationErrorsFn,
178-
formatBindArgsValidationErrors: this.#formatBindArgsValidationErrorsFn,
200+
handleValidationErrorsShape: this.#handleValidationErrorsShape,
201+
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
179202
}).action(serverCodeFn);
180203
}
181204

@@ -197,8 +220,8 @@ export class SafeActionClient<
197220
metadata: this.#metadata,
198221
schema: this.#schema,
199222
bindArgsSchemas: this.#bindArgsSchemas,
200-
formatValidationErrors: this.#formatValidationErrorsFn,
201-
formatBindArgsValidationErrors: this.#formatBindArgsValidationErrorsFn,
223+
handleValidationErrorsShape: this.#handleValidationErrorsShape,
224+
handleBindArgsValidationErrorsShape: this.#handleBindArgsValidationErrorsShape,
202225
}).stateAction(serverCodeFn);
203226
}
204227
}

0 commit comments

Comments
 (0)
Please sign in to comment.