Skip to content

Commit 77484f0

Browse files
authored
Merge pull request #352 from TheEdoRan/fix-validation-errors-v7
2 parents dead2f3 + eef34f8 commit 77484f0

File tree

6 files changed

+92
-17
lines changed

6 files changed

+92
-17
lines changed

packages/next-safe-action/src/__tests__/validation-errors.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,44 @@ test("action with invalid input gives back an object with correct `validationErr
7474
assert.deepStrictEqual(actualResult, expectedResult);
7575
});
7676

77+
test("action with invalid enum input gives back an object with correct `validationErrors` (default formatted shape)", async () => {
78+
const schema = z.object({
79+
foo: z.object({
80+
bar: z.union([z.literal("a"), z.literal("b")]),
81+
}),
82+
baz: z.string().min(3),
83+
});
84+
85+
const action = dac.schema(schema).action(async () => {
86+
return {
87+
ok: true,
88+
};
89+
});
90+
91+
const actualResult = await action({
92+
foo: {
93+
// @ts-expect-error
94+
bar: "c",
95+
},
96+
baz: "a",
97+
});
98+
99+
const expectedResult = {
100+
validationErrors: {
101+
foo: {
102+
bar: {
103+
_errors: ['Invalid literal value, expected "a"', 'Invalid literal value, expected "b"'],
104+
},
105+
},
106+
baz: {
107+
_errors: ["String must contain at least 3 character(s)"],
108+
},
109+
},
110+
};
111+
112+
assert.deepStrictEqual(actualResult, expectedResult);
113+
});
114+
77115
test("action with root level schema error gives back an object with correct `validationErrors` (default formatted shape)", async () => {
78116
const userId = "invalid_uuid";
79117

packages/next-safe-action/src/adapters/types.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,29 +57,47 @@ export interface ValidationAdapter {
5757
validate<S extends Schema>(
5858
schema: S,
5959
data: unknown
60-
): Promise<{ success: true; data: Infer<S> } | { success: false; issues: ValidationIssue[] }>;
60+
): Promise<
61+
| { success: true; data: Infer<S> }
62+
| { success: false; issues: ValidationIssue[]; unionErrors?: { issues: ValidationIssue[] }[] }
63+
>;
6164
// zod
6265
validate<S extends IfInstalled<z.ZodType>>(
6366
schema: S,
6467
data: unknown
65-
): Promise<{ success: true; data: Infer<S> } | { success: false; issues: ValidationIssue[] }>;
68+
): Promise<
69+
| { success: true; data: Infer<S> }
70+
| { success: false; issues: ValidationIssue[]; unionErrors?: { issues: ValidationIssue[] }[] }
71+
>;
6672
// valibot
6773
validate<S extends IfInstalled<GenericSchema>>(
6874
schema: S,
6975
data: unknown
70-
): Promise<{ success: true; data: Infer<S> } | { success: false; issues: ValidationIssue[] }>;
76+
): Promise<
77+
| { success: true; data: Infer<S> }
78+
| { success: false; issues: ValidationIssue[]; unionErrors?: { issues: ValidationIssue[] }[] }
79+
>;
7180
validate<S extends IfInstalled<GenericSchemaAsync>>(
7281
schema: S,
7382
data: unknown
74-
): Promise<{ success: true; data: Infer<S> } | { success: false; issues: ValidationIssue[] }>;
83+
): Promise<
84+
| { success: true; data: Infer<S> }
85+
| { success: false; issues: ValidationIssue[]; unionErrors?: { issues: ValidationIssue[] }[] }
86+
>;
7587
// yup
7688
validate<S extends IfInstalled<YupSchema>>(
7789
schema: S,
7890
data: unknown
79-
): Promise<{ success: true; data: Infer<S> } | { success: false; issues: ValidationIssue[] }>;
91+
): Promise<
92+
| { success: true; data: Infer<S> }
93+
| { success: false; issues: ValidationIssue[]; unionErrors?: { issues: ValidationIssue[] }[] }
94+
>;
8095
// typebox
8196
validate<S extends IfInstalled<TSchema>>(
8297
schema: S,
8398
data: unknown
84-
): Promise<{ success: true; data: Infer<S> } | { success: false; issues: ValidationIssue[] }>;
99+
): Promise<
100+
| { success: true; data: Infer<S> }
101+
| { success: false; issues: ValidationIssue[]; unionErrors?: { issues: ValidationIssue[] }[] }
102+
>;
85103
}

packages/next-safe-action/src/adapters/zod.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ class ZodAdapter implements ValidationAdapter {
3838

3939
return {
4040
success: false,
41-
issues: result.error.issues.map(({ message, path }) => ({ message, path })),
41+
// @ts-expect-error
42+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
43+
issues: result.error.issues.map(({ message, path, unionErrors }) => ({ message, path, unionErrors })),
4244
} as const;
4345
}
4446
}

packages/next-safe-action/src/utils.types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ export type Prettify<T> = {
55

66
// Returns type or promise of type.
77
export type MaybePromise<T> = Promise<T> | T;
8+
9+
// Returns type or array of type.
10+
export type MaybeArray<T> = T | T[];

packages/next-safe-action/src/validation-errors.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
22

3-
import type { Schema, ValidationIssue } from "./adapters/types";
3+
import type { Schema } from "./adapters/types";
44
import type {
55
FlattenedBindArgsValidationErrors,
66
FlattenedValidationErrors,
7+
IssueWithUnionErrors,
78
ValidationErrors,
89
} from "./validation-errors.types";
910

11+
const getIssueMessage = (issue: IssueWithUnionErrors) => {
12+
if (issue.unionErrors) {
13+
return issue.unionErrors.map((u) => u.issues.map((i) => i.message)).flat();
14+
}
15+
return issue.message;
16+
};
17+
1018
// This function is used internally to build the validation errors object from a list of validation issues.
11-
export const buildValidationErrors = <S extends Schema | undefined>(issues: ValidationIssue[]) => {
19+
export const buildValidationErrors = <S extends Schema | undefined>(issues: IssueWithUnionErrors[]) => {
1220
const ve: any = {};
1321

1422
for (const issue of issues) {
15-
const { path, message } = issue;
23+
const { path, message, unionErrors } = issue;
1624

1725
// When path is undefined or empty, set root errors.
1826
if (!path || path.length === 0) {
@@ -37,13 +45,15 @@ export const buildValidationErrors = <S extends Schema | undefined>(issues: Vali
3745
// Key is always the last element of the path.
3846
const key = path[path.length - 1]!;
3947

48+
const issueMessage = getIssueMessage(issue);
49+
4050
// Set error for the current path. If `_errors` array exists, add the message to it.
4151
ref[key] = ref[key]?._errors
4252
? {
4353
...structuredClone(ref[key]),
44-
_errors: [...ref[key]._errors, message],
54+
_errors: [...ref[key]._errors, issueMessage],
4555
}
46-
: { ...structuredClone(ref[key]), _errors: [message] };
56+
: { ...structuredClone(ref[key]), _errors: unionErrors ? issueMessage : [issueMessage] };
4757
}
4858

4959
return ve as ValidationErrors<S>;

packages/next-safe-action/src/validation-errors.types.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
1-
import type { Infer, InferIn, Schema } from "./adapters/types";
2-
import type { Prettify } from "./utils.types";
1+
import type { Infer, InferIn, Schema, ValidationIssue } from "./adapters/types";
2+
import type { MaybeArray, Prettify } from "./utils.types";
33

44
// Basic types and arrays.
55
type NotObject = number | string | boolean | bigint | symbol | null | undefined | any[];
66

77
// Object with an optional list of validation errors.
8-
type VEList = Prettify<{ _errors?: string[] }>;
8+
type VEList<K = undefined> = K extends any[] ? MaybeArray<{ _errors?: string[] }> : { _errors?: string[] };
99

1010
// Creates nested schema validation errors type using recursion.
1111
type SchemaErrors<S> = {
12-
[K in keyof S]?: S[K] extends NotObject ? VEList : Prettify<VEList & SchemaErrors<S[K]>>;
12+
[K in keyof S]?: S[K] extends NotObject ? VEList<S[K]> : Prettify<VEList & SchemaErrors<S[K]>>;
1313
} & {};
1414

15+
export type IssueWithUnionErrors = ValidationIssue & {
16+
unionErrors?: { issues: ValidationIssue[] }[];
17+
};
18+
1519
/**
1620
* Type of the returned object when validation fails.
1721
*/
1822
export type ValidationErrors<S extends Schema | undefined> = S extends Schema
1923
? Infer<S> extends NotObject
20-
? VEList
24+
? Prettify<VEList>
2125
: Prettify<VEList & SchemaErrors<Infer<S>>>
2226
: undefined;
2327

0 commit comments

Comments
 (0)