diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 9b59dec8..c697261a 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -19,6 +19,7 @@ import type * as sessionsExample from "../sessionsExample.js"; import type * as streamsExample from "../streamsExample.js"; import type * as testingFunctions from "../testingFunctions.js"; import type * as triggersExample from "../triggersExample.js"; +import type * as zodFunctionsExample from "../zodFunctionsExample.js"; import type { ApiFromModules, @@ -38,6 +39,7 @@ declare const fullApi: ApiFromModules<{ streamsExample: typeof streamsExample; testingFunctions: typeof testingFunctions; triggersExample: typeof triggersExample; + zodFunctionsExample: typeof zodFunctionsExample; }>; /** diff --git a/convex/zodFunctionsExample.ts b/convex/zodFunctionsExample.ts new file mode 100644 index 00000000..959cb37c --- /dev/null +++ b/convex/zodFunctionsExample.ts @@ -0,0 +1,36 @@ +import { zCustomQuery } from "convex-helpers/server/zod4"; +import { query } from "./_generated/server"; +import { NoOp } from "convex-helpers/server/customFunctions"; +import { z } from "zod/v4"; + +export const zQuery = zCustomQuery(query, NoOp); + +export const noArgs = zQuery({ + args: {}, + handler: async (_ctx) => { + return "Hello world!"; + }, +}); + +const stringToDate = z.codec(z.iso.datetime(), z.date(), { + decode: (isoString) => new Date(isoString), + encode: (date) => date.toISOString(), +}); +const dateToString = z.codec(z.date(), z.iso.datetime(), { + decode: (date) => date.toISOString(), + encode: (isoString) => new Date(isoString), +}); + +export const withArgs = zQuery({ + args: { + date: stringToDate, + }, + returns: { + oneDayAfter: dateToString, + }, + handler: async (_ctx, args) => { + return { + oneDayAfter: new Date(args.date.getTime() + 24 * 60 * 60 * 1000), + }; + }, +}); diff --git a/packages/convex-helpers/server/zod4.functions.test.ts b/packages/convex-helpers/server/zod4.functions.test.ts index f837fc24..711cfb29 100644 --- a/packages/convex-helpers/server/zod4.functions.test.ts +++ b/packages/convex-helpers/server/zod4.functions.test.ts @@ -68,6 +68,13 @@ export const testQuery = zQuery({ }), }); +export const testQueryNoArgs = zQuery({ + args: {}, + handler: async (_ctx, args) => { + assertType>(args); + }, +}); + /** * Test zCustomMutation with Zod schemas for args and return value */ @@ -254,6 +261,7 @@ export const generateUserId = mutation({ const testApi: ApiFromModules<{ fns: { testQuery: typeof testQuery; + testQueryNoArgs: typeof testQueryNoArgs; testMutation: typeof testMutation; testAction: typeof testAction; returnsNothing: typeof returnsNothing; @@ -287,6 +295,22 @@ describe("zCustomQuery, zCustomMutation, zCustomAction", () => { >(); }); + describe("zCustomQuery with no args", () => { + test("through t.query", async () => { + const t = convexTest(schema, modules); + const response = await t.query(testApi.testQueryNoArgs); + expect(response).toBeNull(); + }); + + test("through t.run", async () => { + const t = convexTest(schema, modules); + const response = await t.run((ctx) => + ctx.runQuery(testApi.testQueryNoArgs), + ); + expect(response).toBeNull(); + }); + }); + test("zCustomMutation", async () => { const t = convexTest(schema, modules); const response = await t.mutation(testApi.testMutation, { diff --git a/packages/convex-helpers/server/zod4.ts b/packages/convex-helpers/server/zod4.ts index 4a5f3c4b..f0cd41c3 100644 --- a/packages/convex-helpers/server/zod4.ts +++ b/packages/convex-helpers/server/zod4.ts @@ -960,16 +960,19 @@ type ReturnValueOutput< > = [ReturnsValidator] extends [zCore.$ZodType] ? Returns> : [ReturnsValidator] extends [ZodFields] - ? Returns>> + ? Returns>> : any; // The args before they've been validated: passed from the client type ArgsInput | void> = [ArgsValidator] extends [zCore.$ZodObject] ? [zCore.input] - : [ArgsValidator] extends [ZodFields] - ? [zCore.input>] - : OneArgArray; + : ArgsValidator extends Record + ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type + [{}] + : [ArgsValidator] extends [Record] + ? [zCore.input>] + : OneArgArray; // The args after they've been validated: passed to the handler type ArgsOutput< diff --git a/src/App.tsx b/src/App.tsx index 1ae0ef4b..9f8cf60d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import RelationshipExample from "./components/RelationshipExample"; import SessionsExample from "./components/SessionsExample"; import { HonoExample } from "./components/HonoExample"; import { StreamsExample } from "./components/StreamsExample"; +import { ZodFunctionsExample } from "./components/ZodFunctionsExample"; import { SessionProvider } from "convex-helpers/react/sessions"; import { CacheExample } from "./components/CacheExample"; import { ConvexQueryCacheProvider } from "convex-helpers/react/cache"; @@ -23,6 +24,7 @@ export default function App() { + diff --git a/src/components/ZodFunctionsExample.tsx b/src/components/ZodFunctionsExample.tsx new file mode 100644 index 00000000..86714800 --- /dev/null +++ b/src/components/ZodFunctionsExample.tsx @@ -0,0 +1,42 @@ +import { api } from "../../convex/_generated/api"; +import { useQuery } from "convex/react"; +import { useMemo } from "react"; +import { z } from "zod/v4"; + +const stringToDate = z.codec(z.iso.datetime(), z.date(), { + decode: (isoString) => new Date(isoString), + encode: (date) => date.toISOString(), +}); + +export function ZodFunctionsExample() { + const now = useMemo(() => new Date(), []); + + const noArgsResult = useQuery(api.zodFunctionsExample.noArgs); + const noArgsResultEmptyArgs = useQuery(api.zodFunctionsExample.noArgs, {}); + const withArgsResult = useQuery(api.zodFunctionsExample.withArgs, { + date: stringToDate.encode(now), + }); + + return ( +
+

Zod Functions Example

+
+

No Args Query

+

Result: {noArgsResult ?? "Loading..."}

+
+
+

No Args Query (Empty Args)

+

Result: {noArgsResultEmptyArgs ?? "Loading..."}

+
+
+

With Args Query

+ {withArgsResult && ( +

+ One day after:{" "} + {stringToDate.decode(withArgsResult.oneDayAfter).toLocaleString()} +

+ )} +
+
+ ); +}