Tom Redman
Tom Redman•3w ago

Type error when NOT directly calling function (new to Convex 1.18.2)

I see in the update notes, it's not a good practice to call a function directly from another function. However, when rectifying this (e.g. using ctx.runQuery(myApiQuery) vs myApiQuery() I'm getting the stubborn "Function implicitly has return type 'any' because..." error. Any idea why this is now upsetting the type system? I feel like I'm missing something obvious!
export const getForCurrentUser = query({
args: {},
handler: async (ctx) => {
const { user, team } = await ctx.runQuery(
internal.authentication.user.getCurrentUserOrThrow
);
return await ctx.db
.query("mailchimpLists")
.withIndex("by_team", (q) => q.eq("teamId", team._id))
.collect();
},
});
export const getForCurrentUser = query({
args: {},
handler: async (ctx) => {
const { user, team } = await ctx.runQuery(
internal.authentication.user.getCurrentUserOrThrow
);
return await ctx.db
.query("mailchimpLists")
.withIndex("by_team", (q) => q.eq("teamId", team._id))
.collect();
},
});
And then I have this query:
export const getCurrentUserOrThrow = internalQuery({
args: {},
handler: async (ctx): Promise<{ user: Doc<"users">; team: Doc<"teams"> }> => {
const userId = await getAuthUserId(ctx);
if (!userId) {
throw new Error("Not signed in");
}

const user = await ctx.db.get(userId);

if (!user) {
throw new Error("User not found");
}

const teamId = user.teamId;

if (!teamId) {
throw new Error("User is not in a team");
}

const team = await ctx.db.get(teamId);

if (!team) {
throw new Error("Team not found");
}

return { user, team };
},
});
export const getCurrentUserOrThrow = internalQuery({
args: {},
handler: async (ctx): Promise<{ user: Doc<"users">; team: Doc<"teams"> }> => {
const userId = await getAuthUserId(ctx);
if (!userId) {
throw new Error("Not signed in");
}

const user = await ctx.db.get(userId);

if (!user) {
throw new Error("User not found");
}

const teamId = user.teamId;

if (!teamId) {
throw new Error("User is not in a team");
}

const team = await ctx.db.get(teamId);

if (!team) {
throw new Error("Team not found");
}

return { user, team };
},
});
12 Replies
Convex Bot
Convex Bot•3w ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
lee
lee•3w ago
Best Practices | Convex Developer Hub
This is a list of best practices and common anti-patterns around using Convex.
Actions | Convex Developer Hub
Actions can call third party services to do things such as processing a payment
lee
lee•3w ago
the example in docs is kinda long, so here's a shorter example Before:
export const qInternal = internalQuery({
args: {},
handler: async (ctx) => {
return await ctx.db.query("data").collect();
},
});
export const q = query({
args: {},
handler: async (ctx) => {
await qInternal(ctx);
// more stuff
},
});
export const qInternal = internalQuery({
args: {},
handler: async (ctx) => {
return await ctx.db.query("data").collect();
},
});
export const q = query({
args: {},
handler: async (ctx) => {
await qInternal(ctx);
// more stuff
},
});
After:
async function qInternalHandler(ctx: QueryCtx) {
return await ctx.db.query("data").collect();
}
export const qInternal = internalQuery({
args: {},
handler: qInternalHandler,
});
export const q = query({
args: {},
handler: async (ctx) => {
await qInternalHandler(ctx);
// more stuff
},
});
async function qInternalHandler(ctx: QueryCtx) {
return await ctx.db.query("data").collect();
}
export const qInternal = internalQuery({
args: {},
handler: qInternalHandler,
});
export const q = query({
args: {},
handler: async (ctx) => {
await qInternalHandler(ctx);
// more stuff
},
});
Tom Redman
Tom RedmanOP•3w ago
Thanks @Lee 📿 Really appreciate it. The docs are helpful! I've got some refactoring to do across my code! Love it. Thanks again
lee
lee•3w ago
Best Practices | Convex Developer Hub
This is a list of best practices and common anti-patterns around using Convex.
Tom Redman
Tom RedmanOP•3w ago
I'm combing through all these now. Code already feels a lot cleaner.
export function getCurrentUserOrThrow(
ctx: GenericQueryCtx<DataModel>
): Promise<{ user: Doc<"users">; team: Doc<"teams"> }>;
export function getCurrentUserOrThrow(): Promise<{
user: Doc<"users">;
team: Doc<"teams">;
}>;

export async function getCurrentUserOrThrow(ctx?: GenericQueryCtx<DataModel>) {
if (!ctx) {
return await internalQuery({
args: {},
handler: async (ctx) => {
return await getCurrentUserOrThrow(ctx);
},
});
}

const userId = await getAuthUserId(ctx);
if (!userId) {
throw new Error("Not signed in");
}

const user = await ctx.db.get(userId);
if (!user) {
throw new Error("User not found");
}

const teamId = user.teamId;
if (!teamId) {
throw new Error("User is not in a team");
}

const team = await ctx.db.get(teamId);
if (!team) {
throw new Error("Team not found");
}

return { user, team };
}
export function getCurrentUserOrThrow(
ctx: GenericQueryCtx<DataModel>
): Promise<{ user: Doc<"users">; team: Doc<"teams"> }>;
export function getCurrentUserOrThrow(): Promise<{
user: Doc<"users">;
team: Doc<"teams">;
}>;

export async function getCurrentUserOrThrow(ctx?: GenericQueryCtx<DataModel>) {
if (!ctx) {
return await internalQuery({
args: {},
handler: async (ctx) => {
return await getCurrentUserOrThrow(ctx);
},
});
}

const userId = await getAuthUserId(ctx);
if (!userId) {
throw new Error("Not signed in");
}

const user = await ctx.db.get(userId);
if (!user) {
throw new Error("User not found");
}

const teamId = user.teamId;
if (!teamId) {
throw new Error("User is not in a team");
}

const team = await ctx.db.get(teamId);
if (!team) {
throw new Error("Team not found");
}

return { user, team };
}
Is this legit in the case where I can't pass a query context? (I could also accept a mutation context?) For example, from an action, I can't pass a usable context:
export const create = action({
args: {
creationState: campaignStateSchema,
sendNow: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const { user, team } = await getCurrentUserOrThrow();

// this function req's `fetch` hence we use an action
const { html, plainText } = await parseEmailTemplate(
args.creationState.emailContent.template
);

const campaignName = getCampaignNameFromState(args.creationState);

const campaignId = (await ctx.runMutation(
internal.campaigns.mutations.insert,
{
campaign: //...
}
)) as Id<"campaigns">;

return campaignId;
},
});
export const create = action({
args: {
creationState: campaignStateSchema,
sendNow: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const { user, team } = await getCurrentUserOrThrow();

// this function req's `fetch` hence we use an action
const { html, plainText } = await parseEmailTemplate(
args.creationState.emailContent.template
);

const campaignName = getCampaignNameFromState(args.creationState);

const campaignId = (await ctx.runMutation(
internal.campaigns.mutations.insert,
{
campaign: //...
}
)) as Id<"campaigns">;

return campaignId;
},
});
ballingt
ballingt•3w ago
I don't get it, can you say more? @Tom Redman
Is this legit in the case where I can't pass a query context? (I could also accept a mutation context?)
You can always pass a query context as an argument when you call a function Generally we spell it QueryCtx instead of GenericQueryCtx<DataModel> but either works Ah I see, yes if you're calling a query from an action, you can't just call the function; you need to use runQuery() Cool, neat overloading thing! This looks fine to me, is there a problem here?
lee
lee•3w ago
That if (!ctx) block doesn't look like it would work to me inside of an action, you would use ctx.runQuery
export const create = action({
args: {
creationState: campaignStateSchema,
sendNow: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const { user, team } = await ctx.runQuery(internal.file.getCurrentUserOrThrowQuery);
...
export const create = action({
args: {
creationState: campaignStateSchema,
sendNow: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const { user, team } = await ctx.runQuery(internal.file.getCurrentUserOrThrowQuery);
...
and then
export const getCurrentUserOrThrowQuery = internalQuery({
args: {},
handler: getCurrentUserOrThrow,
});
export const getCurrentUserOrThrowQuery = internalQuery({
args: {},
handler: getCurrentUserOrThrow,
});
the reason convex is making you defined two functions (getCurrentUserOrThrow and getCurrentUserOrThrowQuery in my example), is because one of them is a separate transaction with argument validation, and the other is code running within another transaction
Tom Redman
Tom RedmanOP•3w ago
@Lee yes you are right! And no, no issues @Tom (other than the bug Lee pointed out), just wanted to make sure this is still the blessed path. If query | mutation -> reuse the ctx, if it's an action, you gotta use ActionCtx.runQuery()
lee
lee•3w ago
Btw you won't be able to call internalQuery within a convex function; it won't be registered and can't be called as internal.foo.bar unless it's exported at import-time
Tom Redman
Tom RedmanOP•3w ago
Here's the final form for posterity:
import { getAuthUserId } from "@convex-dev/auth/server";
import { Doc } from "../_generated/dataModel";
import { ActionCtx, internalQuery, QueryCtx } from "../_generated/server";
import { get } from "../base/ query";
import { internal } from "../_generated/api";

export const getCurrentUserOrThrowQuery = internalQuery({
args: {},
handler: getCurrentUserOrThrow,
});

export function getCurrentUserOrThrow(
ctx: QueryCtx
): Promise<{ user: Doc<"users">; team: Doc<"teams"> }>;
export function getCurrentUserOrThrow(
ctx: ActionCtx
): Promise<{ user: Doc<"users">; team: Doc<"teams"> }>;

export async function getCurrentUserOrThrow(ctx: QueryCtx | ActionCtx) {
if (!("db" in ctx)) {
return ctx.runQuery(
internal.authentication.user.getCurrentUserOrThrowQuery
);
}

const userId = await getAuthUserId(ctx);
if (!userId) {
throw new Error("Not signed in");
}

const user = await get(ctx, userId);
if (!user) {
throw new Error("User not found");
}

const teamId = user.teamId;
if (!teamId) {
throw new Error("User is not in a team");
}

const team = await get(ctx, teamId);
if (!team) {
throw new Error("Team not found");
}

return { user, team };
}
import { getAuthUserId } from "@convex-dev/auth/server";
import { Doc } from "../_generated/dataModel";
import { ActionCtx, internalQuery, QueryCtx } from "../_generated/server";
import { get } from "../base/ query";
import { internal } from "../_generated/api";

export const getCurrentUserOrThrowQuery = internalQuery({
args: {},
handler: getCurrentUserOrThrow,
});

export function getCurrentUserOrThrow(
ctx: QueryCtx
): Promise<{ user: Doc<"users">; team: Doc<"teams"> }>;
export function getCurrentUserOrThrow(
ctx: ActionCtx
): Promise<{ user: Doc<"users">; team: Doc<"teams"> }>;

export async function getCurrentUserOrThrow(ctx: QueryCtx | ActionCtx) {
if (!("db" in ctx)) {
return ctx.runQuery(
internal.authentication.user.getCurrentUserOrThrowQuery
);
}

const userId = await getAuthUserId(ctx);
if (!userId) {
throw new Error("Not signed in");
}

const user = await get(ctx, userId);
if (!user) {
throw new Error("User not found");
}

const teamId = user.teamId;
if (!teamId) {
throw new Error("User is not in a team");
}

const team = await get(ctx, teamId);
if (!team) {
throw new Error("Team not found");
}

return { user, team };
}
Is there a better way in Typescript to check if the ctx is either a QueryCtx or an ActionCtx? (Better than checking for the presence of 'db')
erquhart
erquhart•2w ago
I believe runAction is the one method the action ctx has that the other two don't.

Did you find this page helpful?