Skip to content

Commit 6933cbb

Browse files
authored
fix skipConvexValidation - still do zod validation (#874)
<!-- Describe your PR here. --> Fixes #865 <!-- The following applies to third-party contributors. Convex employees and contractors can delete or ignore. --> ---- By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Option to skip automatic Convex validation for custom function builders retained. * **Improvements** * Centralized input/output validation for consistent behavior and clearer error messages. * When skipping Convex validation, argument and return validation behavior is now predictable for Zod-backed custom functions. * **Tests** * Added tests for skip-validation scenarios: extra-arg handling, input-type errors, and invalid return-data errors. * **Chores** * Package version bumped. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents a6099d3 + 8014887 commit 6933cbb

File tree

6 files changed

+96
-14
lines changed

6 files changed

+96
-14
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/convex-helpers/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- Zod support: branded object types are now supported (credit: gari-ix)
6+
- Fixes `skipConvexValidation` for zod custom functions to still do zod validation.
67

78
## 0.1.106
89

packages/convex-helpers/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "convex-helpers",
3-
"version": "0.1.106",
3+
"version": "0.1.107-alpha.0",
44
"description": "A collection of useful code to complement the official convex package.",
55
"type": "module",
66
"bin": {

packages/convex-helpers/server/zod3.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -345,19 +345,25 @@ function customFnBuilder(
345345
customization.input ?? NoOp.input;
346346
const inputArgs = customization.args ?? NoOp.args;
347347
return function customBuilder(fn: any): any {
348-
const { args, handler = fn, returns: maybeObject, ...extra } = fn;
348+
const {
349+
args,
350+
handler = fn,
351+
skipConvexValidation = false,
352+
returns: maybeObject,
353+
...extra
354+
} = fn;
349355

350356
const returns =
351357
maybeObject && !(maybeObject instanceof z.ZodType)
352358
? z.object(maybeObject)
353359
: maybeObject;
354360

355361
const returnValidator =
356-
returns && !fn.skipConvexValidation
362+
returns && !skipConvexValidation
357363
? { returns: zodOutputToConvex(returns) }
358364
: null;
359365

360-
if (args && !fn.skipConvexValidation) {
366+
if (args) {
361367
let argsValidator = args;
362368
if (argsValidator instanceof z.ZodType) {
363369
if (argsValidator instanceof z.ZodObject) {
@@ -371,7 +377,9 @@ function customFnBuilder(
371377
}
372378
const convexValidator = zodToConvexFields(argsValidator);
373379
return builder({
374-
args: addFieldsToValidator(convexValidator, inputArgs),
380+
args: skipConvexValidation
381+
? undefined
382+
: addFieldsToValidator(convexValidator, inputArgs),
375383
...returnValidator,
376384
handler: async (ctx: any, allArgs: any) => {
377385
const added = await customInput(
@@ -404,10 +412,10 @@ function customFnBuilder(
404412
},
405413
});
406414
}
407-
if (Object.keys(inputArgs).length > 0 && !fn.skipConvexValidation) {
415+
if (skipConvexValidation && Object.keys(inputArgs).length > 0) {
408416
throw new Error(
409417
"If you're using a custom function with arguments for the input " +
410-
"customization, you must declare the arguments for the function too.",
418+
"customization, you cannot skip convex validation.",
411419
);
412420
}
413421
return builder({

packages/convex-helpers/server/zod4.functions.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,37 @@ export const transformAsync = zQuery({
182182
}),
183183
});
184184

185+
export const testQueryWithSkipConvexValidation = zQuery({
186+
args: {
187+
name: z.string(),
188+
age: z.number(),
189+
returnBadData: z.boolean().default(false),
190+
},
191+
handler: async (_ctx, args) => {
192+
assertType<{ name: string; age: number }>(args);
193+
const { name, age, returnBadData, ...rest } = args;
194+
if (Object.keys(rest).length > 0) {
195+
throw new Error("extraArg should be dropped");
196+
}
197+
if (returnBadData) {
198+
return {
199+
message: "bad data",
200+
doubledAge: 0n as any,
201+
};
202+
}
203+
return {
204+
message: `Hello ${name}, you are ${age} years old`,
205+
doubledAge: age * 2,
206+
extraArg: "extraArg",
207+
};
208+
},
209+
returns: z.object({
210+
message: z.string(),
211+
doubledAge: z.number(),
212+
}),
213+
skipConvexValidation: true,
214+
});
215+
185216
/**
186217
* Test codec in query args and return value
187218
*/
@@ -264,6 +295,7 @@ const testApi: ApiFromModules<{
264295
testQueryNoArgs: typeof testQueryNoArgs;
265296
testMutation: typeof testMutation;
266297
testAction: typeof testAction;
298+
testQueryWithSkipConvexValidation: typeof testQueryWithSkipConvexValidation;
267299
returnsNothing: typeof returnsNothing;
268300
transform: typeof transform;
269301
transformAsync: typeof transformAsync;
@@ -398,6 +430,39 @@ describe("zCustomQuery, zCustomMutation, zCustomAction", () => {
398430
});
399431
});
400432

433+
describe("skipConvexValidation", () => {
434+
test("zCustomQuery with skipConvexValidation", async () => {
435+
const t = convexTest(schema, modules);
436+
const args = {
437+
name: "Alice",
438+
age: 30,
439+
// Should get dropped
440+
extraArg: "extraArg",
441+
};
442+
const response = await t.query(
443+
testApi.testQueryWithSkipConvexValidation,
444+
args,
445+
);
446+
expect(response).toMatchObject({
447+
message: "Hello Alice, you are 30 years old",
448+
doubledAge: 60,
449+
});
450+
});
451+
test("throwing ConvexError on zod arg validation", async () => {
452+
const t = convexTest(schema, modules);
453+
await expect(
454+
t.query(testApi.testQueryWithSkipConvexValidation, {
455+
name: 123,
456+
} as any),
457+
).rejects.toThrowError(
458+
expect.objectContaining({
459+
data: expect.stringMatching(
460+
/(?=.*"ZodError")(?=.*"name")(?=.*"invalid_type")(?=.*"expected")(?=.*"string")/s,
461+
),
462+
}),
463+
);
464+
});
465+
});
401466
describe("transform", () => {
402467
test("calling a function with synchronous transforms in arguments and return values", async () => {
403468
const t = convexTest(schema, modules);

packages/convex-helpers/server/zod4.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -849,19 +849,25 @@ function customFnBuilder(
849849
customization.input ?? NoOp.input;
850850
const inputArgs = customization.args ?? NoOp.args;
851851
return function customBuilder(fn: any): any {
852-
const { args, handler = fn, returns: maybeObject, ...extra } = fn;
852+
const {
853+
args,
854+
handler = fn,
855+
skipConvexValidation = false,
856+
returns: maybeObject,
857+
...extra
858+
} = fn;
853859

854860
const returns =
855861
maybeObject && !(maybeObject instanceof zCore.$ZodType)
856862
? z.object(maybeObject)
857863
: maybeObject;
858864

859865
const returnValidator =
860-
returns && !fn.skipConvexValidation
866+
returns && !skipConvexValidation
861867
? { returns: zodOutputToConvex(returns) }
862868
: null;
863869

864-
if (args && !fn.skipConvexValidation) {
870+
if (args) {
865871
let argsValidator = args;
866872
if (argsValidator instanceof zCore.$ZodType) {
867873
if (argsValidator instanceof zCore.$ZodObject) {
@@ -875,7 +881,9 @@ function customFnBuilder(
875881
}
876882
const convexValidator = zodToConvexFields(argsValidator);
877883
return builder({
878-
args: addFieldsToValidator(convexValidator, inputArgs),
884+
args: skipConvexValidation
885+
? undefined
886+
: addFieldsToValidator(convexValidator, inputArgs),
879887
...returnValidator,
880888
handler: async (ctx: any, allArgs: any) => {
881889
const added = await customInput(
@@ -908,10 +916,10 @@ function customFnBuilder(
908916
},
909917
});
910918
}
911-
if (Object.keys(inputArgs).length > 0 && !fn.skipConvexValidation) {
919+
if (skipConvexValidation && Object.keys(inputArgs).length > 0) {
912920
throw new Error(
913921
"If you're using a custom function with arguments for the input " +
914-
"customization, you must declare the arguments for the function too.",
922+
"customization, you cannot skip convex validation.",
915923
);
916924
}
917925
return builder({

0 commit comments

Comments
 (0)