#edit a schema object
1 messages · Page 1 of 1 (latest)
Hi @woven raven. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
To add a new boolean field like isLoved to your user schema, you edit convex/schema.ts and update the table definition.
From the docs, schemas are defined with defineSchema, defineTable, and validators from v.[Validators]
Given your current shape, it likely looks something like:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
user: defineTable({
image: v.optional(v.string()),
userId: v.optional(v.string()),
name: v.string(),
email: v.string(),
emailVerified: v.boolean(),
createdAt: v.number(),
updatedAt: v.number(),
}),
});
To add isLoved: boolean, you just add a field:
export default defineSchema({
user: defineTable({
image: v.optional(v.string()),
userId: v.optional(v.string()),
name: v.string(),
email: v.string(),
emailVerified: v.boolean(),
createdAt: v.number(),
updatedAt: v.number(),
isLoved: v.boolean(), // new required field
}),
});
If you already have data in the table, follow the migration pattern: first add it as optional, backfill, then make it required.[Intro migrations; Stateful migrations]
// Step 1: make it optional so existing docs validate
export default defineSchema({
user: defineTable({
image: v.optional(v.string()),
userId: v.optional(v.string()),
name: v.string(),
email: v.string(),
emailVerified: v.boolean(),
createdAt: v.number(),
updatedAt: v.number(),
isLoved: v.optional(v.boolean()),
}),
});
Then run a migration (e.g. using the migrations component) to set a default value on all existing users, and finally change it to isLoved: v.boolean() once all documents have the field.[Migrations component; Data migrations]
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor i want to edit the user table like extending it? like user:{...user, isLoved:v.boolean()}
Let me check the knowledge base for you again. One moment! :books:
You can’t literally write user: { ...user, isLoved: v.boolean() } in defineSchema, but you can achieve the same effect in two common ways:
1. Just edit the table inline (simplest)
In convex/schema.ts, add the field directly to the users table definition.[Schemas; Validators]
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
// existing fields...
name: v.string(),
email: v.string(),
emailVerified: v.boolean(),
createdAt: v.number(),
updatedAt: v.number(),
// new field
isLoved: v.boolean(),
}),
});
If you already have data, add it as optional first and then migrate:[Stateful migrations; Intro migrations]
isLoved: v.optional(v.boolean()),
Run a migration to backfill, then change to v.boolean().
2. “Extend” an object validator
If you have a shared v.object you want to reuse, you can use .extend on it.[VObject.extend]
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
const baseUser = v.object({
name: v.string(),
email: v.string(),
emailVerified: v.boolean(),
createdAt: v.number(),
updatedAt: v.number(),
});
export default defineSchema({
users: defineTable(
baseUser.extend({
isLoved: v.boolean(),
}),
),
});
This is the closest to “extending” the user schema.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor i dont have extends since is not a a object but a table
Let me check the knowledge base for you again. One moment! :books:
You’re right that defineTable({ ... }) takes a plain object, not a v.object, so you don’t see .extend there. The “extending” trick only works if you first define an object validator and then pass that into defineTable, which is supported.[defineTable; Validators reuse]
Here’s how to refactor so you can extend:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
// 1. Define a reusable object validator for the user shape
const userValidator = v.object({
image: v.optional(v.string()),
userId: v.optional(v.string()),
name: v.string(),
email: v.string(),
emailVerified: v.boolean(),
createdAt: v.number(),
updatedAt: v.number(),
});
// 2. Extend it with your new field and use that in defineTable
export default defineSchema({
user: defineTable(
userValidator.extend({
isLoved: v.boolean(), // or v.optional(v.boolean()) during migration
}),
),
});
This is the Convex way to “extend” a table schema: define an object validator, call .extend on it, and pass the result to defineTable instead of using the inline { ... } form.[Validators reuse; Validators]
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor i followed the the sveltekit tutorial to put better auth and mostly work exept for this
const currentUserResponse = useQuery(api.auth.getCurrentUser, () => (isAuthenticated ? {} : 'skip'));
this query cant find im logged and it give me null
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
console.log(await ctx.auth.getUserIdentity())
return authComponent.getAuthUser(ctx);
},
});
Let me check the knowledge base for you again. One moment! :books:
The knowledge sources don’t show this exact SvelteKit + Better Auth issue, but they do describe the same pattern and common failure modes, so I’ll focus on those. [Better-auth SvelteKit usage; Auth debug]
Key points to check:
1. Make sure you’re using Convex’s auth state, not Better Auth’s
In the SvelteKit guide, isAuthenticated comes from useAuth() (the Better Auth wrapper that is wired into Convex via the plugin): [Better-auth SvelteKit usage]
import { useAuth } from "@mmailaender/convex-better-auth-svelte/svelte";
const auth = useAuth();
const isAuthenticated = $derived(auth.isAuthenticated);
const currentUserResponse = useQuery(
api.auth.getCurrentUser,
() => (isAuthenticated ? {} : "skip")
);
If you’re instead using Better Auth’s own useSession() / getSession() or some other flag, Convex may still see you as unauthenticated. The docs explicitly say to rely on Convex’s auth state when gating Convex queries. [Better-auth authorization]
2. Confirm the Convex client is wired to Better Auth
The SvelteKit setup requires:
authClientwith the Convex plugin: [Better-auth SvelteKit installation]
// src/lib/auth-client.ts
import { createAuthClient } from "better-auth/svelte";
import { convexClient } from "@convex-dev/better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [convexClient()],
});
createSvelteAuthClienton the client side, passing thatauthClient: [Better-auth SvelteKit authenticated requests]
import { createSvelteAuthClient } from "@mmailaender/convex-better-auth-svelte/svelte";
import { authClient } from "$lib/auth-client";
createSvelteAuthClient({
authClient,
// optionally: options: { expectAuth: true },
});
If this wiring is missing or misconfigured, Convex won’t receive the Better Auth token, and ctx.auth.getUserIdentity() will log null. [Auth debug]
3. Check what ctx.auth.getUserIdentity() logs
You already log:
console.log(await ctx.auth.getUserIdentity());
return authComponent.getAuthUser(ctx);
Use the Convex dashboard logs to see the output. [Auth debug]
-
If you see
null, Convex is not getting a token:- The Better Auth → Convex plugin path is likely misconfigured (step 2).
- Or the query is running before Convex has validated the token (step 4).
-
If you see a non-null identity, but
authComponent.getAuthUser(ctx)still returnsnullor throws, that’s a Better Auth side issue (e.g. session not found / not linked).
4. Ensure the query only runs after Convex is authenticated
Even if the user is signed in with Better Auth, Convex needs a moment to validate the token. The docs warn that calling authenticated queries too early will see null identity. [Better-auth authorization; Auth debug]
Your pattern:
const currentUserResponse = useQuery(
api.auth.getCurrentUser,
() => (isAuthenticated ? {} : "skip")
);
is correct if isAuthenticated is coming from Convex’s integrated auth state (via useAuth() as in step 1). If you’re using some other flag, the query may run before Convex is ready.
5. Avoid expectAuth while debugging
If you enabled expectAuth: true on the Convex client, all queries are paused until Convex thinks you’re authenticated. This can hide what’s going wrong. The Discord thread suggests turning it off while debugging. [Better-auth SvelteKit authenticated requests; Discord Better Auth + Convex]
6. Verify environment variables and Convex auth config
From the SvelteKit guide: [Better-auth SvelteKit installation]
BETTER_AUTH_SECRETset in Convex env.SITE_URLset in Convex env.PUBLIC_CONVEX_URL,PUBLIC_CONVEX_SITE_URL,PUBLIC_SITE_URLin.env.local.src/convex/auth.config.tsusesgetAuthConfigProvider().
If any of these are wrong, Convex may reject the token and keep you unauthenticated.
If you paste:
- How you create
authClient - How you call
createSvelteAuthClientin Svelte - The actual log output of
ctx.auth.getUserIdentity()
I can map it more directly to the patterns in the docs and narrow down which step is failing.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor in better auuth+convex how can i make a mutation thath eliminate an account?
Let me check the knowledge base for you again. One moment! :books:
There isn’t an explicit “delete account” example in the Better Auth + Convex docs, but you can compose it from the documented pieces:
- Use a Convex mutation.
- Inside it, get the current Better Auth user via the component client.
- Delete any app-side data you want.
- Call a Better Auth API method that deletes the user (e.g.
auth.api.deleteUser), usingauthComponent.getAuthto getauthandheaders.[Component client; Basic usage auth.api]
A mutation skeleton looks like this:
// convex/authAccount.ts
import { mutation } from "./_generated/server";
import { authComponent } from "./auth"; // your createClient(...) export
import { createAuth } from "./auth"; // your betterAuth(...) factory
export const deleteMyAccount = mutation({
args: {},
handler: async (ctx) => {
// 1. Get Better Auth instance + headers for current session
const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
// 2. Get the current Better Auth user
const authUser = await authComponent.getAnyUserById(
ctx,
(await auth.getSession({ headers }))!.user.id
);
if (!authUser) {
throw new Error("No auth user found");
}
// 3. Delete any app-side data tied to this user (example)
// e.g. delete from your own "users" table by authId
const appUser = await ctx.db
.query("users")
.withIndex("authId", (q) => q.eq("authId", authUser._id))
.unique();
if (appUser) {
await ctx.db.delete(appUser._id);
}
// 4. Delete the Better Auth user (API call)
await auth.api.deleteUser({
body: { userId: authUser._id },
headers,
});
},
});
Notes:
authComponent.getAuth(createAuth, ctx)is the documented way to getauthplus the correctheadersfor server-side Better Auth API calls.[Component client getAuth; Basic usage auth.api]- The exact
auth.api.deleteUsersignature isn’t shown in the Convex docs; you’ll need to match it to the Better Auth docs for your version. The pattern of callingauth.api.*({ body, headers })is what’s documented.[Basic usage auth.api]
If you’re instead using triggers to mirror Better Auth users into an app users table, you can also put your app-side cleanup into the user.onDelete trigger so it runs transactionally whenever a Better Auth user is deleted.[Triggers; Update user triggers]
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor i do not have access to the users table since better auth is inside a component
Let me check the knowledge base for you again. One moment! :books:
You’re right that the Better Auth user table lives inside a component, so you can’t access it directly from your app schema. You have two main options, both supported by the Better Auth component client.
1. Delete the Better Auth user via auth.api (no direct table access)
You don’t need direct table access to delete the account; you can call Better Auth’s server API from a Convex mutation using the component client’s getAuth helper. This is the same pattern shown for changePassword in the docs.[Better-auth basic; Component client getAuth]
// convex/deleteAccount.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { authComponent, createAuth } from "./auth"; // your Better Auth wiring
export const deleteMyAccount = mutation({
args: {},
handler: async (ctx) => {
// Get Better Auth instance + headers for current session
const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
// Call the appropriate Better Auth API method to delete the user.
// The exact method name/signature comes from Better Auth docs,
// but the pattern is always:
// auth.api.someMethod({ body: {...}, headers })
//
// Example shape (you must match it to your Better Auth version):
await auth.api.deleteUser({
body: {}, // or { userId: ... } depending on Better Auth
headers,
});
},
});
Key points:
authComponent.getAuth(createAuth, ctx)is the documented way to getauthandheadersfor server-side Better Auth API calls.[Component client getAuth]- You never touch the component’s
usertable directly; Better Auth handles that internally.
You’ll need to look up the exact auth.api.deleteUser signature in the Better Auth docs; the Convex docs only show the pattern, not that specific method.
2. If you need app-side user data: mirror via triggers
If you want to also delete data in your own app tables, the recommended pattern is to mirror the Better Auth user into an app users table using triggers, then clean up that app data when the Better Auth user is deleted.[Triggers; User data mirroring]
Example trigger setup (simplified):
// convex/auth.ts
import { components, internal } from "./_generated/api";
import { createClient, type AuthFunctions } from "@convex-dev/better-auth";
import type { DataModel } from "./_generated/dataModel";
const authFunctions: AuthFunctions = internal.auth;
export const authComponent = createClient<DataModel>(components.betterAuth, {
authFunctions,
triggers: {
user: {
onCreate: async (ctx, authUser) => {
await ctx.db.insert("users", {
email: authUser.email,
authId: authUser._id, // reference to component user
});
},
onDelete: async (ctx, authUser) => {
const user = await ctx.db
.query("users")
.withIndex("authId", (q) => q.eq("authId", authUser._id))
.unique();
if (user) {
await ctx.db.delete(user._id);
}
},
},
},
});
export const { onCreate, onUpdate, onDelete } = authComponent.triggersApi();
Then your deleteMyAccount mutation (option 1) only needs to delete the Better Auth user; the trigger will clean up your app’s users table in the same transaction.[Triggers]
So even though you can’t touch the component’s user table directly, you can:
- Delete the account via
auth.api.*usingauthComponent.getAuth, and - Use triggers to keep your own app tables in sync if needed.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor i also want to use the trigger but it give me an error:
export const authComponent = createClient<DataModel,typeof authSchema>(components.betterAuth,
{
triggers: {
user: {
onCreate: async (ctx, authUser) => {
await ctx.db.insert("users", {
email: authUser.email,
authId: authUser._id, // reference to component user
});
},
},
},
local: {
schema: authSchema,
},
}
Argument of type '{ triggers: { user: { onCreate: <Ctx extends GenericMutationCtx<{ pubblicita: { document: { _id: Id<"pubblicita">; _creationTime: number; image: Id<"_storage">; altImage: string; title: string; description: string; isActive: boolean; }; fieldPaths: "_id" | ExtractFieldPaths<...>; indexes: { ...; }; searchIndexes: {}...' is not assignable to parameter of type '({ local?: { schema?: SchemaDefinition<{ user: TableDefinition<VObject<{ image?: string | null | undefined; userId?: string | null | undefined; permissions?: string[] | undefined; name: string; email: string; emailVerified: boolean; createdAt: number; updatedAt: number; }, { ...; }, "required", "image" | ... 6 more ...'.
Type '{ triggers: { user: { onCreate: <Ctx extends GenericMutationCtx<{ pubblicita: { document: { _id: Id<"pubblicita">; _creationTime: number; image: Id<"_storage">; altImage: string; title: string; description: string; isActive: boolean; }; fieldPaths: "_id" | ExtractFieldPaths<...>; indexes: { ...; }; searchIndexes: {}...' is not assignable to type '{ local?: { schema?: SchemaDefinition<{ user: TableDefinition<VObject<{ image?: string | null | undefined; userId?: string | null | undefined; permissions?: string[] | undefined; name: string; email: string; emailVerified: boolean; createdAt: number; updatedAt: number; }, { ...; }, "required", "image" | ... 6 more ....'.
Property 'authFunctions' is missing in type '{ triggers: { user: { onCrea
Let me check the knowledge base for you again. One moment! :books:
You’re very close—TypeScript is telling you exactly what’s missing: the authFunctions option. For Better Auth triggers to type-check, you must pass authFunctions into createClient, and (for Local Install) also the local.schema. [Migrate to 0.8; Triggers feature; Local install]
From the docs, the correct pattern (adapted to your Local Install) is:
// convex/auth.ts
import { createClient, type AuthFunctions } from "@convex-dev/better-auth";
import { components, internal } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";
import authSchema from "./betterAuth/schema"; // generated by Better Auth CLI
const authFunctions: AuthFunctions = internal.auth;
export const authComponent = createClient<DataModel, typeof authSchema>(
components.betterAuth,
{
authFunctions, // <-- REQUIRED
local: {
schema: authSchema, // <-- Local Install schema
},
triggers: {
user: {
onCreate: async (ctx, authUser) => {
await ctx.db.insert("users", {
email: authUser.email,
authId: authUser._id,
});
},
// you can add onUpdate / onDelete here too
},
},
}
);
// Optional: exported trigger helpers
export const { onCreate, onUpdate, onDelete } = authComponent.triggersApi();
Key points from the docs:
createClienttakes an options object whereauthFunctionsis required when you use triggers. [Migrate to 0.8]- Triggers are configured under
triggers: { user: { onCreate, onUpdate, onDelete } }. [Triggers feature] - With Local Install, you must also pass
local: { schema: authSchema }and usetypeof authSchemaas the second generic. [Local install]
Your error goes away once authFunctions is included alongside triggers and local.schema in that options object.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor i cant deletethe account since there are no headers, how can i delete the account?
triggers: {
user: {
onCreate: async (ctx, registeredUser) => {
console.log("hi")
const user = await ctx.db
.query('user')
.withIndex('by_email', (q) => q.eq('email', registeredUser.email))
.first();
console.log(user)
const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
console.log(auth)
console.log(headers)
//if (!user) {
// const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
// auth.api.deleteUser({
// body: {},
// headers: headers
// });
// return;
//}
//await ctx.db.patch(user._id, {
// id: registeredUser._id,
// name: user.name || registeredUser.name
//});
}
}
Let me check the knowledge base for you again. One moment! :books:
You can’t use authComponent.getAuth(createAuth, ctx) (and therefore auth.api.* / headers) inside triggers. Triggers only get a Convex ctx, not the HTTP request, so there are no cookies/headers to derive a Better Auth session from. The docs only show getAuth being used inside queries/mutations/actions, not triggers.[Component client; Triggers]
So:
- Triggers are for DB-side mirroring/cleanup only (e.g. syncing your own
usertable when Better Auth’s user changes).[Triggers] - Account deletion must be initiated from a Convex function that has headers, via
authComponent.getAuth(createAuth, ctx).
How to delete the account
- Use a Convex mutation that runs in response to a client call (or server action), not a trigger.
- Inside that mutation, call
authComponent.getAuth(createAuth, ctx)and then the Better Auth API to delete the user.
Example pattern (you’ll need to match the exact auth.api method name/signature from Better Auth’s docs):
// convex/account.ts
import { mutation } from "./_generated/server";
import { authComponent, createAuth } from "./auth";
export const deleteMyAccount = mutation({
args: {},
handler: async (ctx) => {
// This has headers because it’s called from the client with cookies
const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
// Optionally: clean up your own app data here using ctx.db
// Delete the Better Auth user (method name/signature from Better Auth docs)
await auth.api.deleteUser({
body: {}, // or { userId: ... } depending on your Better Auth version
headers,
});
},
});
Then call deleteMyAccount from the client after the user confirms deletion.
What triggers are for
Your trigger should not try to call auth.api.deleteUser. Instead, use it to keep your own tables in sync when Better Auth changes its own data, for example:
triggers: {
user: {
onCreate: async (ctx, registeredUser) => {
// mirror into your own "user" table, etc.
},
onDelete: async (ctx, registeredUser) => {
// delete from your own "user" table, etc.
},
},
},
The actual Better Auth account deletion should be done via a mutation as above, not from the trigger.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor i need to delete the account o creation cause i dont want to accept it
Let me check the knowledge base for you again. One moment! :books:
You’re running into a limitation of how Better Auth + Convex are wired: triggers don’t have headers, so you cannot call authComponent.getAuth(createAuth, ctx) or any auth.api.* method from inside a trigger. Triggers are only for DB-side mirroring/cleanup. [Triggers]
So you cannot “reject” a user by calling auth.api.deleteUser from user.onCreate.
You have two realistic options, depending on what “don’t want to accept it” means for your app:
1. Soft‑reject in your own tables (recommended)
Let Better Auth create the user, but immediately mark them as rejected / inactive in your app’s users table via the trigger. You then treat them as “non‑users” everywhere.
Example trigger:
export const authComponent = createClient<DataModel, typeof authSchema>(
components.betterAuth,
{
authFunctions,
local: { schema: authSchema },
triggers: {
user: {
onCreate: async (ctx, authUser) => {
// Mirror into your own table with a "rejected" flag
await ctx.db.insert("users", {
authId: authUser._id,
email: authUser.email,
status: "rejected", // or isAccepted: false
});
},
},
},
}
);
Then in your app logic, always check status / isAccepted and block access if they’re rejected. This is exactly the kind of thing triggers are meant for: transactional sync between Better Auth and your own tables. [Triggers; User mirroring]
2. Defer actual deletion to a mutation
If you truly must delete the Better Auth user (not just mark them rejected), you need to do it from a Convex function that has headers, not from the trigger. [Component client getAuth; Basic usage auth.api]
A common pattern:
- In
user.onCreate, store enough info in your own table to know this user is “pending review”. - After your review logic (maybe in a background job or admin UI), call a mutation that:
- Uses
authComponent.getAuth(createAuth, ctx)to get{ auth, headers }. - Calls the appropriate
auth.api.*method to delete that Better Auth user. - Cleans up your own tables.
- Uses
Example mutation skeleton (you must match the exact Better Auth API from their docs; Convex docs only show the pattern):
export const rejectUser = mutation({
args: { authId: v.id("user") },
handler: async (ctx, args) => {
const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
// Delete Better Auth user – method name/signature from Better Auth docs
await auth.api.deleteUser({
body: { userId: args.authId }, // or whatever your version expects
headers,
});
// Clean up your own app data
const appUser = await ctx.db
.query("users")
.withIndex("authId", (q) => q.eq("authId", args.authId))
.unique();
if (appUser) {
await ctx.db.delete(appUser._id);
}
},
});
You cannot move this logic into the trigger because triggers never see headers. [Triggers]
Summary
- Triggers: use them to mirror/sync your own tables (e.g. mark user as rejected, delete app data on
onDelete). They cannot callauth.api.*because there are no headers. [Triggers] - Actual Better Auth account deletion: must be done in a Convex query/mutation/action that has headers via
authComponent.getAuth(createAuth, ctx). [Component client getAuth; Basic usage auth.api]
Given that constraint, the practical solution for “don’t want to accept it on creation” is to soft‑reject in your own tables in onCreate, and only hard‑delete via a separate mutation if you really need to.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor what is the best way to handle this query?
export const getAllUrl = internalQuery({
args: {
images: v.array(v.id('image'))
},
handler: async (ctx, { images }) => {
ctx.db.query("image").withIndex("by_id",(q)=>q.eq("_id",))
//return images.map((id) => ctx.storage.getUrl(id));
}
});
Let me check the knowledge base for you again. One moment! :books:
You don’t need a DB query here at all; you already have the storage IDs in images, so just map them to URLs with ctx.storage.getUrl and Promise.all. That’s exactly how the docs show serving files from storage.[Serve files; StorageReader.getUrl]
import { internalQuery } from "./_generated/server";
import { v } from "convex/values";
export const getAllUrl = internalQuery({
args: {
images: v.array(v.id("_storage")), // or your own type if you wrap it
},
handler: async (ctx, { images }) => {
return Promise.all(
images.map((id) => ctx.storage.getUrl(id))
);
},
});
If your images arg is actually IDs from an image table (not storage IDs), then:
- Fetch the docs by ID (or via an index if you have one).
- Extract the storage ID field.
- Call
ctx.storage.getUrlon those.
But if images is already Id<"_storage">, the direct getUrl approach above is the simplest and most efficient.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor why i cant do somthing like this:
export const getAllUrl = internalQuery({
args: {
images: v.array(v.id('image'))
},
handler: async (ctx, { images }) => {
const allImages = await Promise.all(images.map((id) => ctx.db.get(id)));
return Promise.all(allImages.map((img) => ctx.storage.getUrl(img!.image)));
}
});
export const getAll = query({
args: {},
handler: async (ctx) => {
const pubblicita = await ctx.db
.query('pubblicita')
.withIndex('by_deleted', (q) => q.eq('deleted', false))
.collect();
const imagesUrl = await ctx.runQuery(internal.image.query.getAllUrl, {
images: pubblicita.map((p) => p.image)
});
return [pubblicita,imagesUrl]
}
});
getAll give me an error
Let me check the knowledge base for you again. One moment! :books:
You’re very close; the pattern you’re using is valid in Convex, but there are two common gotchas that can cause getAll to fail:
- Type mismatch on
imagesargument - Nulls from
db.getor fromstorage.getUrl
From the knowledge sources:
db.gettakes a table name and an id:ctx.db.get("tasks", args.id).[Reading data]storage.getUrltakes anId<"_storage">and returnsPromise<null | string>.[StorageReader.getUrl]- Internal queries are called from actions/mutations via
ctx.runQuery(internal.someFn, args).[Internal functions]
Let’s walk through your code and tighten it up.
1. Make sure images is the right id type
Your internal query:
export const getAllUrl = internalQuery({
args: {
images: v.array(v.id('image'))
},
handler: async (ctx, { images }) => {
const allImages = await Promise.all(images.map((id) => ctx.db.get(id)));
return Promise.all(allImages.map((img) => ctx.storage.getUrl(img!.image)));
}
});
Issues to check:
v.id('image')means each element isId<"image">, i.e. a document id from theimagetable.ctx.db.getmust be called asctx.db.get("image", id), notctx.db.get(id). [Reading data]
So the handler should be:
export const getAllUrl = internalQuery({
args: {
images: v.array(v.id("image")),
},
handler: async (ctx, { images }) => {
const allImages = await Promise.all(
images.map((id) => ctx.db.get("image", id))
);
// allImages: (Doc<"image"> | null)[]
return Promise.all(
allImages.map((img) =>
img ? ctx.storage.getUrl(img.image) : Promise.resolve(null)
)
);
},
});
Note the null‑check: `db.get` can return `null` if a document doesn’t exist.[[Reading data](<https://docs.convex.dev/database/reading-data>)]
Also ensure that `img.image` is actually an `Id<"_storage">` in your schema; `storage.getUrl` expects that type.[[Serve files](<https://docs.convex.dev/file-storage/serve-files#generating-file-urls-in-queries>); [StorageReader.getUrl](<https://docs.convex.dev/api/interfaces/server.StorageReader#geturl>)]
### 2. Calling the internal query
Your public query:
```ts
export const getAll = query({
args: {},
handler: async (ctx) => {
const pubblicita = await ctx.db
.query('pubblicita')
.withIndex('by_deleted', (q) => q.eq('deleted', false))
.collect();
const imagesUrl = await ctx.runQuery(internal.image.query.getAllUrl, {
images: pubblicita.map((p) => p.image)
});
return [pubblicita, imagesUrl];
}
});
This is structurally fine, assuming:
p.imageis of typeId<"image">(matchingv.id("image")ingetAllUrl).- You imported
internalfrom./_generated/apiand the pathinternal.image.query.getAllUrlmatches your file/export name.[Internal functions]
If p.image is actually an Id<"_storage">, then your internal query should instead accept v.id("_storage") and skip the db.get entirely:
export const getAllUrl = internalQuery({
args: {
images: v.array(v.id("_storage")),
},
handler: async (ctx, { images }) => {
return Promise.all(images.map((id) => ctx.storage.getUrl(id)));
},
});
Then images: pubblicita.map((p) => p.image) must be Id<"_storage"> values.
3. Why getAll “gives an error”
The knowledge sources don’t show your exact runtime/TS error, so I can’t say precisely which of these is failing, but based on the docs the likely causes are:
ctx.db.get(id)instead ofctx.db.get("image", id).[Reading data]- Type mismatch between
p.imageandv.id("image")/v.id("_storage"). - Not handling
nullfromdb.getorstorage.getUrl.[StorageReader.getUrl]
If you paste the exact error message, I can map it directly to one of these, but the “best” version of your pattern, grounded in the docs, is one of the two handlers above depending on whether images are table ids or storage ids.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@woven raven Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@earnest condor if i do not return imagesUrl i dont get any error and the type is (string | null)[]
but if i try to return it the return type become any
Let me check the knowledge base for you again. One moment! :books:
This is TypeScript’s circular type inference issue, the same one Convex documents for actions and that also shows up with ctx.runQuery / ctx.runMutation / ctx.runAction and internal functions.[Circular inference; Discord circular; Discord tools]
Pattern:
- If you use the result of
ctx.runQuery(...)(or an internal query) in your return value, TS tries to infer the outer function’s return type from the inner one. - But the inner one’s type itself comes from Convex’s generated types, which depend on the outer function.
- TS gives up and falls back to
any.
Convex’s docs show two ways to break this loop; both work for your getAll query.
Option 1: Type the handler’s return
export const getAll = query({
args: {},
handler: async (ctx): Promise<[Doc<"pubblicita">[], (string | null)[]]> => {
const pubblicita = await ctx.db
.query("pubblicita")
.withIndex("by_deleted", (q) => q.eq("deleted", false))
.collect();
const imagesUrl = await ctx.runQuery(internal.image.query.getAllUrl, {
images: pubblicita.map((p) => p.image),
});
return [pubblicita, imagesUrl];
},
});
(Adjust Doc<"pubblicita"> import from ./_generated/dataModel as needed.)[TS best practices]
Option 2: Type the intermediate result
export const getAll = query({
args: {},
handler: async (ctx) => {
const pubblicita = await ctx.db
.query("pubblicita")
.withIndex("by_deleted", (q) => q.eq("deleted", false))
.collect();
const imagesUrl: (string | null)[] = await ctx.runQuery(
internal.image.query.getAllUrl,
{ images: pubblicita.map((p) => p.image) }
);
return [pubblicita, imagesUrl];
},
});
This is exactly the workaround Convex recommends for actions calling queries; the same reasoning applies here.[[Circular inference](<https://docs.convex.dev/functions/actions#dealing-with-circular-type-inference>); [Discord action calling action](<https://discord.com/channels/1019350475847499849/1414623864079515658>); [Discord tools](<https://discord.com/channels/1019350475847499849/1425094023550599260>)]
TypeScript will still check that `(string | null)[]` matches what `getAllUrl` actually returns, so you don’t lose type safety.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------