#Best way to take relation field
8 messages · Page 1 of 1 (latest)
Hi @worldly raft. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
The standard pattern in Convex is to query your posts, then use ctx.db.get to fetch the related author for each post inside a Promise.all (or asyncMap). Here's how it looks:
import { query } from "./_generated/server";
import { v } from "convex/values";
export const listPostsWithAuthor = query({
args: {},
handler: async (ctx) => {
const posts = await ctx.db.query("posts").collect();
return Promise.all(
posts.map(async (post) => {
const author = await ctx.db.get(post.authorId);
return {
...post,
authorName: author?.name ?? "Unknown",
};
})
);
},
});
This is the idiomatic Convex approach — fetch the list, then do a db.get per related document in parallel via Promise.all. [Loading users by ID]
A note on performance
It may seem like many DB calls compared to a SQL JOIN, but Convex already handles caching under the hood, so these parallel db.get calls are efficient in practice. [Discord thread]
Using convex-helpers for cleaner code
If you want a more readable abstraction, the convex-helpers package provides a getManyFrom helper for back-references and getAll for direct ID lookups:
import { getAll } from "convex-helpers/server/relationships";
const posts = await ctx.db.query("posts").collect();
const authorIds = posts.map((p) => p.authorId);
const authors = await getAll(ctx.db, authorIds);
// authors[i] corresponds to posts[i]
Note:
getAllreturnsnullin place of any document that doesn't exist. UsegetAllOrThrowif you want an exception instead.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@worldly raft Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@red seal and if i have pagination also?
export const list = query({
args: {
tenantId: v.id("tenants"),
paginationOpts: paginationOptsValidator,
kind: v.optional(ruleKindValidator),
isActive: v.optional(v.boolean()),
search: v.optional(v.string()),
},
handler: async (
ctx,
{ tenantId, kind, search, isActive, paginationOpts },
) => {
await requireTenantAccess(ctx, tenantId);
// Stage 1: Pick the table to query.
const rulesQuery: QueryInitializer<DataModel["rules"]> =
ctx.db.query("rules");
if (search !== undefined) {
return await rulesQuery
.withSearchIndex("search_rules", (q) => {
const base = q.search("name", search).eq("tenantId", tenantId);
return kind !== undefined
? isActive !== undefined
? base.eq("kind", kind).eq("isActive", isActive)
: base.eq("kind", kind)
: isActive !== undefined
? base.eq("isActive", isActive)
: base;
})
.paginate(paginationOpts);
}
// Stage 2: Pick the index to use.
let indexedQuery: Query<DataModel["rules"]> = rulesQuery;
if (kind !== undefined) {
indexedQuery = rulesQuery.withIndex("by_tenant_kind_active", (q) => {
const base = q.eq("tenantId", tenantId).eq("kind", kind);
return isActive !== undefined ? base.eq("isActive", isActive) : base;
});
} else {
indexedQuery = rulesQuery.withIndex("by_tenant_active", (q) => {
const base = q.eq("tenantId", tenantId);
return isActive !== undefined ? base.eq("isActive", isActive) : base;
});
}
// Stage 3: Apply ordering.
let orderedQuery: OrderedQuery<DataModel["rules"]> = indexedQuery;
orderedQuery = indexedQuery.order("asc");
const rules = await orderedQuery.paginate(paginationOpts);
return rules;
},
});
Let me check the knowledge base for you again. One moment! :books:
To add the authorName join to a paginated query, you transform the page array of the result returned by .paginate() before returning it. The key is that .paginate() returns an object with a page property (the array of documents), and you can map over that array to enrich each document with related data.
Here's how to apply it to your query:
const paginatedRules = await orderedQuery.paginate(paginationOpts);
return {
...paginatedRules,
page: await Promise.all(
paginatedRules.page.map(async (rule) => {
const author = await ctx.db.get(rule.authorId);
return {
...rule,
authorName: author?.name ?? "Unknown",
};
})
),
};
And similarly for the search branch:
if (search !== undefined) {
const paginatedRules = await rulesQuery
.withSearchIndex("search_rules", (q) => { /* ... */ })
.paginate(paginationOpts);
return {
...paginatedRules,
page: await Promise.all(
paginatedRules.page.map(async (rule) => {
const author = await ctx.db.get(rule.authorId);
return {
...rule,
authorName: author?.name ?? "Unknown",
};
})
),
};
}
The pattern is: spread the pagination result (...paginatedRules) to preserve the cursor and isDone/continueCursor fields, then replace page with your transformed array. [Paginated Queries]