314 lines
8.0 KiB
TypeScript
314 lines
8.0 KiB
TypeScript
// @ts-ignore TS6133
|
|
import { expect, test } from "vitest";
|
|
|
|
import * as z from "zod/v3";
|
|
import { ZodIssueCode } from "../ZodError.js";
|
|
import { util } from "../helpers/util.js";
|
|
|
|
test("refinement", () => {
|
|
const obj1 = z.object({
|
|
first: z.string(),
|
|
second: z.string(),
|
|
});
|
|
const obj2 = obj1.partial().strict();
|
|
|
|
const obj3 = obj2.refine((data) => data.first || data.second, "Either first or second should be filled in.");
|
|
|
|
expect(obj1 === (obj2 as any)).toEqual(false);
|
|
expect(obj2 === (obj3 as any)).toEqual(false);
|
|
|
|
expect(() => obj1.parse({})).toThrow();
|
|
expect(() => obj2.parse({ third: "adsf" })).toThrow();
|
|
expect(() => obj3.parse({})).toThrow();
|
|
obj3.parse({ first: "a" });
|
|
obj3.parse({ second: "a" });
|
|
obj3.parse({ first: "a", second: "a" });
|
|
});
|
|
|
|
test("refinement 2", () => {
|
|
const validationSchema = z
|
|
.object({
|
|
email: z.string().email(),
|
|
password: z.string(),
|
|
confirmPassword: z.string(),
|
|
})
|
|
.refine((data) => data.password === data.confirmPassword, "Both password and confirmation must match");
|
|
|
|
expect(() =>
|
|
validationSchema.parse({
|
|
email: "aaaa@gmail.com",
|
|
password: "aaaaaaaa",
|
|
confirmPassword: "bbbbbbbb",
|
|
})
|
|
).toThrow();
|
|
});
|
|
|
|
test("refinement type guard", () => {
|
|
const validationSchema = z.object({
|
|
a: z.string().refine((s): s is "a" => s === "a"),
|
|
});
|
|
type Input = z.input<typeof validationSchema>;
|
|
type Schema = z.infer<typeof validationSchema>;
|
|
|
|
util.assertEqual<"a", Input["a"]>(false);
|
|
util.assertEqual<string, Input["a"]>(true);
|
|
|
|
util.assertEqual<"a", Schema["a"]>(true);
|
|
util.assertEqual<string, Schema["a"]>(false);
|
|
});
|
|
|
|
test("refinement Promise", async () => {
|
|
const validationSchema = z
|
|
.object({
|
|
email: z.string().email(),
|
|
password: z.string(),
|
|
confirmPassword: z.string(),
|
|
})
|
|
.refine(
|
|
(data) => Promise.resolve().then(() => data.password === data.confirmPassword),
|
|
"Both password and confirmation must match"
|
|
);
|
|
|
|
await validationSchema.parseAsync({
|
|
email: "aaaa@gmail.com",
|
|
password: "password",
|
|
confirmPassword: "password",
|
|
});
|
|
});
|
|
|
|
test("custom path", async () => {
|
|
const result = await z
|
|
.object({
|
|
password: z.string(),
|
|
confirm: z.string(),
|
|
})
|
|
.refine((data) => data.confirm === data.password, { path: ["confirm"] })
|
|
.spa({ password: "asdf", confirm: "qewr" });
|
|
expect(result.success).toEqual(false);
|
|
if (!result.success) {
|
|
expect(result.error.issues[0].path).toEqual(["confirm"]);
|
|
}
|
|
});
|
|
|
|
test("use path in refinement context", async () => {
|
|
const noNested = z.string()._refinement((_val, ctx) => {
|
|
if (ctx.path.length > 0) {
|
|
ctx.addIssue({
|
|
code: ZodIssueCode.custom,
|
|
message: `schema cannot be nested. path: ${ctx.path.join(".")}`,
|
|
});
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
const data = z.object({
|
|
foo: noNested,
|
|
});
|
|
|
|
const t1 = await noNested.spa("asdf");
|
|
const t2 = await data.spa({ foo: "asdf" });
|
|
|
|
expect(t1.success).toBe(true);
|
|
expect(t2.success).toBe(false);
|
|
if (t2.success === false) {
|
|
expect(t2.error.issues[0].message).toEqual("schema cannot be nested. path: foo");
|
|
}
|
|
});
|
|
|
|
test("superRefine", () => {
|
|
const Strings = z.array(z.string()).superRefine((val, ctx) => {
|
|
if (val.length > 3) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.too_big,
|
|
maximum: 3,
|
|
type: "array",
|
|
inclusive: true,
|
|
exact: true,
|
|
message: "Too many items 😡",
|
|
});
|
|
}
|
|
|
|
if (val.length !== new Set(val).size) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `No duplicates allowed.`,
|
|
});
|
|
}
|
|
});
|
|
|
|
const result = Strings.safeParse(["asfd", "asfd", "asfd", "asfd"]);
|
|
|
|
expect(result.success).toEqual(false);
|
|
if (!result.success) expect(result.error.issues.length).toEqual(2);
|
|
|
|
Strings.parse(["asfd", "qwer"]);
|
|
});
|
|
|
|
test("superRefine async", async () => {
|
|
const Strings = z.array(z.string()).superRefine(async (val, ctx) => {
|
|
if (val.length > 3) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.too_big,
|
|
maximum: 3,
|
|
type: "array",
|
|
inclusive: true,
|
|
exact: true,
|
|
message: "Too many items 😡",
|
|
});
|
|
}
|
|
|
|
if (val.length !== new Set(val).size) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `No duplicates allowed.`,
|
|
});
|
|
}
|
|
});
|
|
|
|
const result = await Strings.safeParseAsync(["asfd", "asfd", "asfd", "asfd"]);
|
|
|
|
expect(result.success).toEqual(false);
|
|
if (!result.success) expect(result.error.issues.length).toEqual(2);
|
|
|
|
Strings.parseAsync(["asfd", "qwer"]);
|
|
});
|
|
|
|
test("superRefine - type narrowing", () => {
|
|
type NarrowType = { type: string; age: number };
|
|
const schema = z
|
|
.object({
|
|
type: z.string(),
|
|
age: z.number(),
|
|
})
|
|
.nullable()
|
|
.superRefine((arg, ctx): arg is NarrowType => {
|
|
if (!arg) {
|
|
// still need to make a call to ctx.addIssue
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "cannot be null",
|
|
fatal: true,
|
|
});
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
util.assertEqual<z.infer<typeof schema>, NarrowType>(true);
|
|
|
|
expect(schema.safeParse({ type: "test", age: 0 }).success).toEqual(true);
|
|
expect(schema.safeParse(null).success).toEqual(false);
|
|
});
|
|
|
|
test("chained mixed refining types", () => {
|
|
type firstRefinement = { first: string; second: number; third: true };
|
|
type secondRefinement = { first: "bob"; second: number; third: true };
|
|
type thirdRefinement = { first: "bob"; second: 33; third: true };
|
|
const schema = z
|
|
.object({
|
|
first: z.string(),
|
|
second: z.number(),
|
|
third: z.boolean(),
|
|
})
|
|
.nullable()
|
|
.refine((arg): arg is firstRefinement => !!arg?.third)
|
|
.superRefine((arg, ctx): arg is secondRefinement => {
|
|
util.assertEqual<typeof arg, firstRefinement>(true);
|
|
if (arg.first !== "bob") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "`first` property must be `bob`",
|
|
});
|
|
return false;
|
|
}
|
|
return true;
|
|
})
|
|
.refine((arg): arg is thirdRefinement => {
|
|
util.assertEqual<typeof arg, secondRefinement>(true);
|
|
return arg.second === 33;
|
|
});
|
|
|
|
util.assertEqual<z.infer<typeof schema>, thirdRefinement>(true);
|
|
});
|
|
|
|
test("get inner type", () => {
|
|
z.string()
|
|
.refine(() => true)
|
|
.innerType()
|
|
.parse("asdf");
|
|
});
|
|
|
|
test("chained refinements", () => {
|
|
const objectSchema = z
|
|
.object({
|
|
length: z.number(),
|
|
size: z.number(),
|
|
})
|
|
.refine(({ length }) => length > 5, {
|
|
path: ["length"],
|
|
message: "length greater than 5",
|
|
})
|
|
.refine(({ size }) => size > 7, {
|
|
path: ["size"],
|
|
message: "size greater than 7",
|
|
});
|
|
const r1 = objectSchema.safeParse({
|
|
length: 4,
|
|
size: 9,
|
|
});
|
|
expect(r1.success).toEqual(false);
|
|
if (!r1.success) expect(r1.error.issues.length).toEqual(1);
|
|
|
|
const r2 = objectSchema.safeParse({
|
|
length: 4,
|
|
size: 3,
|
|
});
|
|
expect(r2.success).toEqual(false);
|
|
if (!r2.success) expect(r2.error.issues.length).toEqual(2);
|
|
});
|
|
|
|
test("fatal superRefine", () => {
|
|
const Strings = z
|
|
.string()
|
|
.superRefine((val, ctx) => {
|
|
if (val === "") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "foo",
|
|
fatal: true,
|
|
});
|
|
}
|
|
})
|
|
.superRefine((val, ctx) => {
|
|
if (val !== " ") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "bar",
|
|
});
|
|
}
|
|
});
|
|
|
|
const result = Strings.safeParse("");
|
|
|
|
expect(result.success).toEqual(false);
|
|
if (!result.success) expect(result.error.issues.length).toEqual(1);
|
|
});
|
|
|
|
test("superRefine after skipped transform", () => {
|
|
const schema = z
|
|
.string()
|
|
.regex(/^\d+$/)
|
|
.transform((val) => Number(val))
|
|
.superRefine((val) => {
|
|
if (typeof val !== "number") {
|
|
throw new Error("Called without transform");
|
|
}
|
|
});
|
|
|
|
const result = schema.safeParse("");
|
|
|
|
expect(result.success).toEqual(false);
|
|
});
|