Skip to content

Commit 795d257

Browse files
thomasballingerConvex, Inc.
authored and
Convex, Inc.
committed
Allow objects to be used as args and return type validators (#27145)
Allow objects with validator properties to be used as argument and return value validators. Now these two both work: ``` export const oldSyntax = mutation({ args: { arg: v.string() }, handler: (_, args) => ... }); export const newSyntax = mutation({ args: v.object({ arg: v.string() }), handler: (_, args) => ... }); ``` This is a change some types of middleware will need to deal with: when adding or removing fields from args it is now necessary to check whether `args` is a validator or an object and use different logic accordingly. The recommended path here is to use the new `asObjectValidator` function which accepts either a record of string to validator or a validator and always returns a validator. GitOrigin-RevId: 610d1251d4321697b9e7e75081a403c4cd3e3026
1 parent ef9465d commit 795d257

File tree

6 files changed

+194
-38
lines changed

6 files changed

+194
-38
lines changed

npm-packages/convex/src/server/impl/registration_impl.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { functionName } from "../api.js";
3939
import { extractReferencePath } from "../components/index.js";
4040
import { parseArgs } from "../../common/index.js";
4141
import { performAsyncSyscall } from "./syscall.js";
42+
import { asObjectValidator } from "../../values/validator.js";
4243

4344
async function invokeMutation<
4445
F extends (ctx: GenericMutationCtx<GenericDataModel>, ...args: any) => any,
@@ -127,8 +128,8 @@ function assertNotBrowser() {
127128
type FunctionDefinition =
128129
| ((ctx: any, args: DefaultFunctionArgs) => any)
129130
| {
130-
args?: Record<string, GenericValidator>;
131-
returns?: GenericValidator;
131+
args?: GenericValidator | Record<string, GenericValidator>;
132+
returns?: GenericValidator | Record<string, GenericValidator>;
132133
handler: (ctx: any, args: DefaultFunctionArgs) => any;
133134
};
134135

@@ -139,7 +140,7 @@ function exportArgs(functionDefinition: FunctionDefinition) {
139140
typeof functionDefinition === "object" &&
140141
functionDefinition.args !== undefined
141142
) {
142-
args = v.object(functionDefinition.args);
143+
args = asObjectValidator(functionDefinition.args);
143144
}
144145
return JSON.stringify(args.json);
145146
};
@@ -152,7 +153,7 @@ function exportReturns(functionDefinition: FunctionDefinition) {
152153
typeof functionDefinition === "object" &&
153154
functionDefinition.returns !== undefined
154155
) {
155-
returns = functionDefinition.returns;
156+
returns = asObjectValidator(functionDefinition.returns);
156157
}
157158
return JSON.stringify(returns ? returns.json : null);
158159
};

npm-packages/convex/src/server/registration.test.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { test } from "@jest/globals";
22
import { assert, Equals } from "../test/type_testing.js";
33
import { v } from "../values/validator.js";
4-
import { ApiFromModules, DefaultFunctionArgs } from "./index.js";
4+
import {
5+
ApiFromModules,
6+
DefaultFunctionArgs,
7+
mutationGeneric,
8+
} from "./index.js";
59
import { EmptyObject, MutationBuilder } from "./registration.js";
610

711
describe("argument inference", () => {
@@ -270,3 +274,102 @@ describe("argument inference", () => {
270274
assert<Equals<Args, ExpectedArgs>>;
271275
});
272276
});
277+
278+
describe("argument and return value validators can be objects or validators", () => {
279+
// Test with mutation, but all the wrappers work the same way.
280+
const mutation: MutationBuilder<any, "public"> = mutationGeneric;
281+
282+
const module = {
283+
configArgsObject: mutation({
284+
args: {
285+
arg: v.string(),
286+
},
287+
handler: (_, args) => {
288+
assert<Equals<(typeof args)["arg"], string>>;
289+
return "result";
290+
},
291+
}),
292+
configArgsValidatorIsNotSupported: mutation({
293+
args: v.object({
294+
arg: v.string(),
295+
}),
296+
handler: (_, args) => {
297+
assert<Equals<(typeof args)["arg"], string>>;
298+
return "result";
299+
},
300+
}),
301+
configOutputObject: mutation({
302+
returns: {
303+
arg: v.string(),
304+
},
305+
handler: () => {
306+
return { arg: "result" };
307+
},
308+
}),
309+
configOutputValidator: mutation({
310+
returns: v.object({
311+
arg: v.string(),
312+
}),
313+
handler: () => {
314+
return { arg: "result" };
315+
},
316+
}),
317+
};
318+
type API = ApiFromModules<{ module: typeof module }>;
319+
320+
const expectedArgsExport = {
321+
type: "object",
322+
value: {
323+
arg: {
324+
fieldType: {
325+
type: "string",
326+
},
327+
optional: false,
328+
},
329+
},
330+
};
331+
332+
const expectedReturnsExport = {
333+
type: "object",
334+
value: {
335+
arg: {
336+
fieldType: {
337+
type: "string",
338+
},
339+
optional: false,
340+
},
341+
},
342+
};
343+
344+
test("config with args validator", () => {
345+
type Args = API["module"]["configArgsObject"]["_args"];
346+
type ExpectedArgs = { arg: string };
347+
assert<Equals<Args, ExpectedArgs>>;
348+
const argsString = module.configArgsObject.exportArgs();
349+
expect(JSON.parse(argsString)).toEqual(expectedArgsExport);
350+
});
351+
352+
test("config with args object", () => {
353+
type Args = API["module"]["configArgsValidatorIsNotSupported"]["_args"];
354+
type ExpectedArgs = { arg: string };
355+
assert<Equals<Args, ExpectedArgs>>;
356+
const argsString = module.configArgsObject.exportArgs();
357+
expect(JSON.parse(argsString)).toEqual(expectedArgsExport);
358+
});
359+
360+
test("config with output validator", () => {
361+
type ReturnType = API["module"]["configOutputObject"]["_returnType"];
362+
type Expected = { arg: string };
363+
assert<Equals<ReturnType, Expected>>;
364+
const returnString = module.configOutputObject.exportReturns();
365+
expect(JSON.parse(returnString)).toEqual(expectedReturnsExport);
366+
});
367+
368+
test("config with output object", () => {
369+
type ReturnType = API["module"]["configOutputValidator"]["_returnType"];
370+
type Expected = { arg: string };
371+
assert<Equals<ReturnType, Expected>>;
372+
const returnString = module.configOutputValidator.exportReturns();
373+
expect(JSON.parse(returnString)).toEqual(expectedReturnsExport);
374+
});
375+
});

npm-packages/convex/src/server/registration.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import {
1212
OptionalRestArgs,
1313
ValidatorTypeToReturnType,
1414
} from "../server/api.js";
15-
import { Infer, ObjectType, PropertyValidators } from "../values/validator.js";
15+
import {
16+
GenericValidator,
17+
Infer,
18+
ObjectType,
19+
PropertyValidators,
20+
} from "../values/validator.js";
1621
import { Id } from "../values/value.js";
1722
import {
1823
GenericDataModel,
@@ -514,22 +519,28 @@ export interface ValidatedFunction<
514519
*/
515520

516521
export type ReturnValueForOptionalValidator<
517-
ReturnsValidator extends Validator<any, any, any> | void,
522+
ReturnsValidator extends Validator<any, any, any> | PropertyValidators | void,
518523
> = [ReturnsValidator] extends [Validator<any, any, any>]
519524
? ValidatorTypeToReturnType<Infer<ReturnsValidator>>
520-
: any;
525+
: [ReturnsValidator] extends [PropertyValidators]
526+
? ObjectType<ReturnsValidator>
527+
: any;
521528

522529
export type ArgsArrayForOptionalValidator<
523-
ArgsValidator extends PropertyValidators | void,
524-
> = [ArgsValidator] extends [PropertyValidators]
525-
? OneArgArray<ObjectType<ArgsValidator>>
526-
: ArgsArray;
530+
ArgsValidator extends GenericValidator | PropertyValidators | void,
531+
> = [ArgsValidator] extends [Validator<any, any, any>]
532+
? OneArgArray<Infer<ArgsValidator>>
533+
: [ArgsValidator] extends [PropertyValidators]
534+
? OneArgArray<ObjectType<ArgsValidator>>
535+
: ArgsArray;
527536

528537
export type DefaultArgsForOptionalValidator<
529-
ArgsValidator extends PropertyValidators | void,
530-
> = [ArgsValidator] extends [PropertyValidators]
531-
? [ObjectType<ArgsValidator>]
532-
: OneArgArray;
538+
ArgsValidator extends GenericValidator | PropertyValidators | void,
539+
> = [ArgsValidator] extends [Validator<any, any, any>]
540+
? [Infer<ArgsValidator>]
541+
: [ArgsValidator] extends [PropertyValidators]
542+
? [ObjectType<ArgsValidator>]
543+
: OneArgArray;
533544

534545
/**
535546
* Internal type helper used by Convex code generation.
@@ -542,8 +553,11 @@ export type MutationBuilder<
542553
Visibility extends FunctionVisibility,
543554
> = {
544555
<
545-
ArgsValidator extends PropertyValidators | void,
546-
ReturnsValidator extends Validator<any, any, any> | void,
556+
ArgsValidator extends PropertyValidators | Validator<any, any, any> | void,
557+
ReturnsValidator extends
558+
| PropertyValidators
559+
| Validator<any, any, any>
560+
| void,
547561
ReturnValue extends ReturnValueForOptionalValidator<ReturnsValidator> = any,
548562
OneOrZeroArgs extends
549563
ArgsArrayForOptionalValidator<ArgsValidator> = DefaultArgsForOptionalValidator<ArgsValidator>,

npm-packages/convex/src/values/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ export type {
1313
Value,
1414
NumericValue,
1515
} from "./value.js";
16-
export { v } from "./validator.js";
16+
export { v, asObjectValidator } from "./validator.js";
1717
export type {
18-
PropertyValidators,
19-
ObjectType,
18+
AsObjectValidator,
2019
GenericValidator,
20+
ObjectType,
21+
PropertyValidators,
2122
} from "./validator.js";
2223
export type {
2324
ValidatorJSON,

npm-packages/convex/src/values/validator.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,46 @@ import {
2727
export type GenericValidator = Validator<any, any, any>;
2828

2929
export function isValidator(v: any): v is GenericValidator {
30-
return !!v.isValidator;
30+
return !!v.isConvexValidator;
3131
}
3232

33+
/**
34+
* Coerce an object with validators as properties to a validator.
35+
* If a validator is passed, return it.
36+
*
37+
* @public
38+
*/
39+
export function asObjectValidator<
40+
V extends Validator<any, any, any> | PropertyValidators,
41+
>(
42+
obj: V,
43+
): V extends Validator<any, any, any>
44+
? V
45+
: V extends PropertyValidators
46+
? Validator<ObjectType<V>>
47+
: never {
48+
if (isValidator(obj)) {
49+
return obj as any;
50+
} else {
51+
return v.object(obj as PropertyValidators) as any;
52+
}
53+
}
54+
55+
/**
56+
* Coerce an object with validators as properties to a validator.
57+
* If a validator is passed, return it.
58+
*
59+
* @public
60+
*/
61+
export type AsObjectValidator<
62+
V extends Validator<any, any, any> | PropertyValidators,
63+
> =
64+
V extends Validator<any, any, any>
65+
? V
66+
: V extends PropertyValidators
67+
? Validator<ObjectType<V>>
68+
: never;
69+
3370
/**
3471
* The validator builder.
3572
*

0 commit comments

Comments
 (0)