Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zCustomFunction doesn't return a type when using runMutation/runQuery etc. #504

Open
MatteusSteinberg opened this issue Mar 18, 2025 · 12 comments

Comments

@MatteusSteinberg
Copy link

Since updating to Convex 1.20^ it has become "illegal" to execute Convex functions as a normal Typescript function ( await fooBar(ctx, { args }) ) so the only correct approach is to run internal and external functions with the action helpers ( await runQuery(internal.foo.bar.fooBar, { args }) )

When running runMutation(api/internal....) for a zCustomFunction it returns any, but when using the default built-in Convex function, the correct types get returned.

Example of failing/missing type actions:

/searchAgent/external.ts

import { action } from '../utils/customWrapper'
import { internal } from '../_generated/api'

export default action({
  args: {
    searchAgentId: zid('searchAgents')
  },
  handler: async (ctx, { searchAgentId }) => {
    const { runQuery } = ctx
    return  await runQuery(internal.searchAgent.index.get, { searchAgentId })
  }
})

/searchAgent/index.ts

import { zid } from 'convex-helpers/server/zod'
import { internalQuery } from '../utils/customWrapper'

export const get = internalQuery({
  args: {
    searchAgentId: zid('searchAgents')
  },
  handler: async (ctx, { searchAgentId }) => {
    const { db } = ctx

    const searchAgent = await db.get(searchAgentId)

    return searchAgent
  }
})

/utils/customWrapper.ts

import { zCustomAction, zCustomMutation, zCustomQuery } from 'convex-helpers/server/zod'
import { v } from 'convex/values'
import {
  action as convexAction,
  internalAction as convexInternalAction,
  internalMutation as convexInternalMutation,
  internalQuery as convexInternalQuery,
  mutation as convexMutation,
  query as convexQuery
} from '../_generated/server'

export const mutation = zCustomMutation(convexMutation, {
  args: {
    posthogDistinctId: v.optional(v.string())
  },
  input: async (ctx, { posthogDistinctId }) => {
    return { ctx: { ...ctx, posthogDistinctId }, args: {} }
  }
})

export const query = zCustomQuery(convexQuery, {
  args: {
    posthogDistinctId: v.optional(v.string())
  },
  input: async (ctx, { posthogDistinctId }) => {
    return { ctx: { ...ctx, posthogDistinctId }, args: {} }
  }
})

export const action = zCustomAction(convexAction, {
  args: {
    posthogDistinctId: v.optional(v.string())
  },
  input: async (ctx, { posthogDistinctId }) => {
    return { ctx: { ...ctx, posthogDistinctId }, args: {} }
  }
})

export const internalMutation = zCustomMutation(convexInternalMutation, {
  args: {
    posthogDistinctId: v.optional(v.string())
  },
  input: async (ctx, { posthogDistinctId }) => {
    return { ctx: { ...ctx, posthogDistinctId }, args: {} }
  }
})

export const internalQuery = zCustomQuery(convexInternalQuery, {
  args: {
    posthogDistinctId: v.optional(v.string())
  },
  input: async (ctx, { posthogDistinctId }) => {
    return { ctx: { ...ctx, posthogDistinctId }, args: {} }
  }
})

export const internalAction = zCustomAction(convexInternalAction, {
  args: {
    posthogDistinctId: v.optional(v.string())
  },
  input: async (ctx, { posthogDistinctId }) => {
    return { ctx: { ...ctx, posthogDistinctId }, args: {} }
  }
})
@ianmacartney
Copy link
Collaborator

ianmacartney commented Mar 18, 2025 via email

@ianmacartney
Copy link
Collaborator

ianmacartney commented Mar 18, 2025 via email

@MatteusSteinberg
Copy link
Author

Any update on this?

@ianmacartney
Copy link
Collaborator

ianmacartney commented Mar 22, 2025

Any update on this?

@MatteusSteinberg - it should be fixed in the latest version of convex-helpers. Please re-open if not!

@MatteusSteinberg
Copy link
Author

@ianmacartney - I'm running v0.1.75 and the type is still not inferred

@ianmacartney
Copy link
Collaborator

Sorry to hear that, I can try to reproduce it again later this week, but it was working for me. If you made a repo with a minimal reproduction it'd be quicker to debug. Thanks!

@ianmacartney
Copy link
Collaborator

@MatteusSteinberg the issue you're seeing is due to circular type references, not due to the zod helpers. I copied your functions into a project and you don't get the issue if you take out this:

return  await runQuery(internal.searchAgent.index.get, { searchAgentId })

Unfortunately even though they're in different files, they all import from api which imports from all the files, so when their output type depends on another function's output type it get tough. You can break the cycle by annotating the return type of the function like this:

export default action({
  args: {
    searchAgentId: zid("searchAgents"),
  },
  handler: async (ctx, { searchAgentId }): Promise<Doc<"searchAgents"> | null> => {
    const { runQuery } = ctx;
    return await runQuery(internal.zodExample.get, { searchAgentId });
  },
});

@MatteusSteinberg
Copy link
Author

This tanks the developer experience of using the zod helpers if I have to run through my whole codebase and start annotating the return type for each function manually.

Do you plan on having the issue fixed or will it be like this for the foreseeable future if I want to use the zod helpers?

@ianmacartney
Copy link
Collaborator

ianmacartney commented Mar 26, 2025 via email

@ianmacartney
Copy link
Collaborator

ianmacartney commented Mar 26, 2025 via email

@MatteusSteinberg
Copy link
Author

I have split the code into multiple files and still have the same issue where no type is returned from the function. I've tried breaking up the dependencies across different files as suggested, setting a return validator, annotating a return type but I'm still experiencing the same problem with the type information not being properly propagated.

Why should the issue be circular dependency?

@ianmacartney
Copy link
Collaborator

Here's some resources:
https://docs.convex.dev/functions/actions#dealing-with-circular-type-inference
get-convex/convex-js#3
https://discord.com/channels/1019350475847499849/1195158111665799248

Here's a repro with your code that I fixed by adding a return type: https://github.com/get-convex/convex-helpers/blob/repro/zod-return-type/convex/zod2.ts#L21
So I'm guessing it's some return type that needs to get annotated. You can bisect the issue by commenting out code.

The cycle is

  1. function A export depends on value of internal.foo.otherFunction.
  2. internal depends on every file export.
  3. file export type depends on function A export.

There are efforts to do some static codegen to break the cycle, and I have other ideas, but this is how it works currently. Sorry!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants