Skip to content

Commit 7bde00b

Browse files
Add vRequired / VRequired utils in validators.ts (#866)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: nicolas@convex.dev <nicolas@convex.dev>
1 parent edc4613 commit 7bde00b

File tree

3 files changed

+242
-105
lines changed

3 files changed

+242
-105
lines changed

packages/convex-helpers/server/zod4.ts

Lines changed: 5 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ import type {
4040
import { pick, type Expand } from "../index.js";
4141
import type { Customization, Registration } from "./customFunctions.js";
4242
import { NoOp } from "./customFunctions.js";
43-
import { addFieldsToValidator } from "../validators.js";
43+
import {
44+
addFieldsToValidator,
45+
vRequired,
46+
type VRequired,
47+
} from "../validators.js";
4448

4549
// #region Convex function definition with Zod
4650

@@ -1882,110 +1886,6 @@ const _zidRegistry = zCore.registry<{ tableName: string }>();
18821886

18831887
type NotUndefined<T> = Exclude<T, undefined>;
18841888

1885-
type VRequired<T extends Validator<any, OptionalProperty, any>> =
1886-
T extends VId<infer Type, OptionalProperty>
1887-
? VId<NotUndefined<Type>, "required">
1888-
: T extends VString<infer Type, OptionalProperty>
1889-
? VString<NotUndefined<Type>, "required">
1890-
: T extends VFloat64<infer Type, OptionalProperty>
1891-
? VFloat64<NotUndefined<Type>, "required">
1892-
: T extends VInt64<infer Type, OptionalProperty>
1893-
? VInt64<NotUndefined<Type>, "required">
1894-
: T extends VBoolean<infer Type, OptionalProperty>
1895-
? VBoolean<NotUndefined<Type>, "required">
1896-
: T extends VNull<infer Type, OptionalProperty>
1897-
? VNull<NotUndefined<Type>, "required">
1898-
: T extends VAny<infer Type, OptionalProperty>
1899-
? VAny<NotUndefined<Type>, "required">
1900-
: T extends VLiteral<infer Type, OptionalProperty>
1901-
? VLiteral<NotUndefined<Type>, "required">
1902-
: T extends VBytes<infer Type, OptionalProperty>
1903-
? VBytes<NotUndefined<Type>, "required">
1904-
: T extends VObject<
1905-
infer Type,
1906-
infer Fields,
1907-
OptionalProperty,
1908-
infer FieldPaths
1909-
>
1910-
? VObject<
1911-
NotUndefined<Type>,
1912-
Fields,
1913-
"required",
1914-
FieldPaths
1915-
>
1916-
: T extends VArray<
1917-
infer Type,
1918-
infer Element,
1919-
OptionalProperty
1920-
>
1921-
? VArray<NotUndefined<Type>, Element, "required">
1922-
: T extends VRecord<
1923-
infer Type,
1924-
infer Key,
1925-
infer Value,
1926-
OptionalProperty,
1927-
infer FieldPaths
1928-
>
1929-
? VRecord<
1930-
NotUndefined<Type>,
1931-
Key,
1932-
Value,
1933-
"required",
1934-
FieldPaths
1935-
>
1936-
: T extends VUnion<
1937-
infer Type,
1938-
infer Members,
1939-
OptionalProperty,
1940-
infer FieldPaths
1941-
>
1942-
? VUnion<
1943-
NotUndefined<Type>,
1944-
Members,
1945-
"required",
1946-
FieldPaths
1947-
>
1948-
: never;
1949-
1950-
function vRequired(validator: GenericValidator) {
1951-
const { kind, isOptional } = validator;
1952-
if (isOptional === "required") {
1953-
return validator;
1954-
}
1955-
1956-
switch (kind) {
1957-
case "id":
1958-
return v.id(validator.tableName);
1959-
case "string":
1960-
return v.string();
1961-
case "float64":
1962-
return v.float64();
1963-
case "int64":
1964-
return v.int64();
1965-
case "boolean":
1966-
return v.boolean();
1967-
case "null":
1968-
return v.null();
1969-
case "any":
1970-
return v.any();
1971-
case "literal":
1972-
return v.literal(validator.value);
1973-
case "bytes":
1974-
return v.bytes();
1975-
case "object":
1976-
return v.object(validator.fields);
1977-
case "array":
1978-
return v.array(validator.element);
1979-
case "record":
1980-
return v.record(validator.key, validator.value);
1981-
case "union":
1982-
return v.union(...validator.members);
1983-
default:
1984-
kind satisfies never;
1985-
throw new Error("Unknown Convex validator type: " + kind);
1986-
}
1987-
}
1988-
19891889
type TableNameFromType<T> =
19901890
T extends GenericId<infer TableName> ? TableName : string;
19911891

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { v } from "convex/values";
2+
import { describe, expect, test, expectTypeOf } from "vitest";
3+
import { vRequired, type VRequired } from "./validators.js";
4+
import type { Equals } from "./index.js";
5+
import type { Validator } from "convex/values";
6+
7+
describe("vRequired", () => {
8+
test("returns required validator unchanged", () => {
9+
testVRequired(v.string(), v.string());
10+
});
11+
12+
test("converts optional string to required", () => {
13+
testVRequired(v.optional(v.string()), v.string());
14+
});
15+
16+
test("converts optional number to required", () => {
17+
testVRequired(v.optional(v.float64()), v.float64());
18+
});
19+
20+
test("converts optional boolean to required", () => {
21+
testVRequired(v.optional(v.boolean()), v.boolean());
22+
});
23+
24+
test("converts optional int64 to required", () => {
25+
testVRequired(v.optional(v.int64()), v.int64());
26+
});
27+
28+
test("converts optional null to required", () => {
29+
testVRequired(v.optional(v.null()), v.null());
30+
});
31+
32+
test("converts optional any to required", () => {
33+
testVRequired(v.optional(v.any()), v.any());
34+
});
35+
36+
test("converts optional literal to required", () => {
37+
testVRequired(v.optional(v.literal("test")), v.literal("test"));
38+
});
39+
40+
test("converts optional bytes to required", () => {
41+
testVRequired(v.optional(v.bytes()), v.bytes());
42+
});
43+
44+
test("converts optional object to required", () => {
45+
testVRequired(
46+
v.optional(v.object({ name: v.string() })),
47+
v.object({ name: v.string() }),
48+
);
49+
});
50+
51+
test("converts optional array to required", () => {
52+
testVRequired(v.optional(v.array(v.string())), v.array(v.string()));
53+
});
54+
55+
test("converts optional record to required", () => {
56+
testVRequired(
57+
v.optional(v.record(v.string(), v.number())),
58+
v.record(v.string(), v.number()),
59+
);
60+
});
61+
62+
test("converts optional union to required", () => {
63+
testVRequired(
64+
v.optional(v.union(v.string(), v.number())),
65+
v.union(v.string(), v.number()),
66+
);
67+
});
68+
69+
test("converts optional id to required", () => {
70+
testVRequired(v.optional(v.id("users")), v.id("users"));
71+
});
72+
});
73+
74+
function testVRequired<
75+
T extends Validator<any, any, any>,
76+
Expected extends Validator<any, "required", any>,
77+
>(
78+
input: T,
79+
expected: Expected &
80+
(Equals<Expected, VRequired<T>> extends true
81+
? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
82+
{}
83+
: "Expected type must match VRequired<Input>"),
84+
) {
85+
const result = vRequired(input);
86+
expect(result).toEqual(expected);
87+
expect(result.isOptional).toBe("required");
88+
// This is redundant with the type check in the argument, but good for sanity
89+
expectTypeOf(result).toEqualTypeOf(expected as any);
90+
}

packages/convex-helpers/validators.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,18 @@ import type {
1414
OptionalProperty,
1515
PropertyValidators,
1616
Validator,
17+
VAny,
18+
VArray,
19+
VBoolean,
20+
VBytes,
21+
VFloat64,
1722
VId,
23+
VInt64,
1824
VLiteral,
25+
VNull,
1926
VObject,
2027
VOptional,
28+
VRecord,
2129
VString,
2230
VUnion,
2331
} from "convex/values";
@@ -889,3 +897,142 @@ function stripUnknownFields<T extends Validator<any, any, any>>(
889897
function appendPath(opts: { _pathPrefix?: string } | undefined, path: string) {
890898
return opts?._pathPrefix ? `${opts._pathPrefix}.${path}` : path;
891899
}
900+
901+
type NotUndefined<T> = Exclude<T, undefined>;
902+
903+
/**
904+
* A type that converts an optional validator to a required validator.
905+
*
906+
* This is the inverse of `VOptional`. It takes a validator that may be optional
907+
* and returns the equivalent required validator type.
908+
*
909+
* @example
910+
* ```ts
911+
* type OptionalString = VOptional<VString<string, "required">>;
912+
* type RequiredString = VRequired<OptionalString>; // VString<string, "required">
913+
* ```
914+
*/
915+
export type VRequired<T extends Validator<any, OptionalProperty, any>> =
916+
T extends VId<infer Type, OptionalProperty>
917+
? VId<NotUndefined<Type>, "required">
918+
: T extends VString<infer Type, OptionalProperty>
919+
? VString<NotUndefined<Type>, "required">
920+
: T extends VFloat64<infer Type, OptionalProperty>
921+
? VFloat64<NotUndefined<Type>, "required">
922+
: T extends VInt64<infer Type, OptionalProperty>
923+
? VInt64<NotUndefined<Type>, "required">
924+
: T extends VBoolean<infer Type, OptionalProperty>
925+
? VBoolean<NotUndefined<Type>, "required">
926+
: T extends VNull<infer Type, OptionalProperty>
927+
? VNull<NotUndefined<Type>, "required">
928+
: T extends VAny<infer Type, OptionalProperty>
929+
? VAny<NotUndefined<Type>, "required">
930+
: T extends VLiteral<infer Type, OptionalProperty>
931+
? VLiteral<NotUndefined<Type>, "required">
932+
: T extends VBytes<infer Type, OptionalProperty>
933+
? VBytes<NotUndefined<Type>, "required">
934+
: T extends VObject<
935+
infer Type,
936+
infer Fields,
937+
OptionalProperty,
938+
infer FieldPaths
939+
>
940+
? VObject<
941+
NotUndefined<Type>,
942+
Fields,
943+
"required",
944+
FieldPaths
945+
>
946+
: T extends VArray<
947+
infer Type,
948+
infer Element,
949+
OptionalProperty
950+
>
951+
? VArray<NotUndefined<Type>, Element, "required">
952+
: T extends VRecord<
953+
infer Type,
954+
infer Key,
955+
infer Value,
956+
OptionalProperty,
957+
infer FieldPaths
958+
>
959+
? VRecord<
960+
NotUndefined<Type>,
961+
Key,
962+
Value,
963+
"required",
964+
FieldPaths
965+
>
966+
: T extends VUnion<
967+
infer Type,
968+
infer Members,
969+
OptionalProperty,
970+
infer FieldPaths
971+
>
972+
? VUnion<
973+
NotUndefined<Type>,
974+
Members,
975+
"required",
976+
FieldPaths
977+
>
978+
: never;
979+
980+
/**
981+
* Converts an optional validator to a required validator.
982+
*
983+
* This is the inverse of `v.optional()`. It takes a validator that may be optional
984+
* and returns the equivalent required validator.
985+
*
986+
* ```ts
987+
* const optionalString = v.optional(v.string());
988+
* const requiredString = vRequired(optionalString); // v.string()
989+
*
990+
* // Already required validators are returned as-is
991+
* const alreadyRequired = v.string();
992+
* const stillRequired = vRequired(alreadyRequired); // v.string()
993+
* ```
994+
*
995+
* @param validator The validator to make required.
996+
* @returns A required version of the validator.
997+
*/
998+
export function vRequired<T extends Validator<any, OptionalProperty, any>>(
999+
validator: T,
1000+
): VRequired<T> {
1001+
const { kind, isOptional } = validator;
1002+
if (isOptional === "required") {
1003+
// TypeScript can't prove T is already VRequired<T>, so we go via unknown.
1004+
return validator as unknown as VRequired<T>;
1005+
}
1006+
1007+
switch (kind) {
1008+
case "id":
1009+
return v.id(validator.tableName) as VRequired<T>;
1010+
case "string":
1011+
return v.string() as VRequired<T>;
1012+
case "float64":
1013+
return v.float64() as VRequired<T>;
1014+
case "int64":
1015+
return v.int64() as VRequired<T>;
1016+
case "boolean":
1017+
return v.boolean() as VRequired<T>;
1018+
case "null":
1019+
return v.null() as VRequired<T>;
1020+
case "any":
1021+
return v.any() as VRequired<T>;
1022+
case "literal":
1023+
return v.literal(validator.value) as VRequired<T>;
1024+
case "bytes":
1025+
return v.bytes() as VRequired<T>;
1026+
case "object":
1027+
return v.object(validator.fields) as VRequired<T>;
1028+
case "array":
1029+
return v.array(validator.element) as VRequired<T>;
1030+
case "record":
1031+
return v.record(validator.key, validator.value) as VRequired<T>;
1032+
case "union":
1033+
return v.union(...validator.members) as VRequired<T>;
1034+
default:
1035+
kind satisfies never;
1036+
throw new Error("Unknown Convex validator type: " + kind);
1037+
}
1038+
}

0 commit comments

Comments
 (0)