ian
ian•2y ago

Performance impact of re-using queries

@erquhart : I'm handling authorization by creating reusable queries that ensure authorization for the data they return, and each of them reuse this query:
export const getUser = async (auth: Auth, db: DatabaseReader) => {
const identity = await auth.getUserIdentity()
if (!identity) {
throw new Error('Not authenticated')
}
const user = await db
.query('users')
.withIndex('byClerkId', (q) => q.eq('clerkId', identity.subject))
.unique()
if (!user) {
throw new Error('User not found')
}
return user
}
export const getUser = async (auth: Auth, db: DatabaseReader) => {
const identity = await auth.getUserIdentity()
if (!identity) {
throw new Error('Not authenticated')
}
const user = await db
.query('users')
.withIndex('byClerkId', (q) => q.eq('clerkId', identity.subject))
.unique()
if (!user) {
throw new Error('User not found')
}
return user
}
I don't know much about Convex's runtime, would a lot of identical calls to queries like this be expected to impact response times?
5 Replies
ian
ianOP•2y ago
Great question- there's a few things to consider here, apologies for the brain dump: 1. I'd expect each of those indexed calls to fetch a single document with an index to be in the single-digit ms, I use 1ms when doing back-of-the-envelope math. So if you're only calling it a handful of times per server function, and mostly in parallel, it shouldn't have a noticeable impact. 2. We will likely optimize calls like this in the future to have a consistent cache for the query to make these calls essentially free after the first one. 3. For db.get, we already cache results, so calling it many times with the same ID is basically free. So if you passed around userId instead of auth, you'd have a faster lookup today. 4. You might be interested in my post on a withUser middleware-esque wrapper, and then pass the user around. [^1] 5. You might be more interested in Lee's post on adding row-level security by wrapping the db object to do certain checks on objects in certain tables.[^2] A fun fact here is that you can compose it with withUser so each rule you write for access controls will have a user object present to do auth checks against. 6. FYI: If you schedule a mutation or action, the auth object will not be passed along, so you'd want to pass any credentials explicitly (and make sure to think about the case where the user's access might change before the scheduled function executes) If you're talking about only doing this call once per query/mutation, a lot of the above doesn't apply - you shouldn't notice an impact of doing a user lookup for each function, per the performance estimate I noted in (1).
ian
ianOP•2y ago
Authentication: Wrappers as “Middleware”
Using wrapper functions like withUser can help you organize your code into middleware-like blocks that you can compose to keep your function logic con...
ian
ianOP•2y ago
Row Level Security: Wrappers as "Middleware"
Implementing row-level security on Convex as a library. Wrap access to the database with access checks written in plain old JS / TS.
erquhart
erquhart•2y ago
Ah this is super helpful, thank you. I considered passing around id as I figured point queries would be cached, but then each caller has to run that query to get the user id themselves anyway. I did see that row level security write up mentioned elsewhere, I'll take a look at that and withUser too. Thanks again 🍻
ian
ianOP•2y ago
No problem. And as a reminder for passing around userId: I'd only pass this around within the server functions. Allowing a client function to pass up a userId for the logged-in user would be a security concern. You're probably already aware of that, but wanted to clarify for future readers. Each server function that requires authentication should probably start off doing the authentication lookup