igor9silva
igor9silvaβ€’2mo ago

Shared functionality patterns and authz

being honest, it feels very not ideal, as i'll need to be constantly thinking about it e.g I have a setStatus function to patch 1 field. I need it to be it's own mutation so I can call it from an action (LLM calling) with runMutation(), but I also want it to be a pure function so I can call it efficiently from another mutation.
export const setStatus = internalMutation({
args: {
actionId: v.id('taskActions'),
status: taskActionStatuses,
},
handler: async (ctx, { actionId, status }) => {
await ctx.db.patch(actionId, {
status,
isDone: status === 'succeeded' || status === 'failed' || status === 'cancelled',
});
},
});
export const setStatus = internalMutation({
args: {
actionId: v.id('taskActions'),
status: taskActionStatuses,
},
handler: async (ctx, { actionId, status }) => {
await ctx.db.patch(actionId, {
status,
isDone: status === 'succeeded' || status === 'failed' || status === 'cancelled',
});
},
});
If I were able to use runQuery() efficiently (read as equal to calling the function directly) from other queries, or just calling it straight as I did with currentUser above (even better!), I would not have to worry about splitting it into two functions ever. It would just work and actually force me to abstract properly (it happened a few times before I found out I should not be doing it πŸ˜…). it's at least a missed opportunity ------------------------------------ BTW thank you guys for being so open and attentive here, I hope I'm not being too much of a bother
12 Replies
erquhart
erquhartβ€’2mo ago
@igor9silva here works When you say "I'll need to be constantly thinking about it", can you say more on that?
igor9silva
igor9silvaOPβ€’2mo ago
for sure
erquhart
erquhartβ€’2mo ago
also "It would just work and actually force me to abstract properly"
igor9silva
igor9silvaOPβ€’2mo ago
disclaimer: I have a bunch of use cases where I will call queries and mutations from actions AND from other mutations, maybe that's not common?
erquhart
erquhartβ€’2mo ago
super common πŸ‘ I have to step away for a bit so I'll share this pre-emptively:

Once at scale and beyond fast early prototyping, the mindset switch I embraced was to write my backend as plain Typescript functions that accept a context and interact with the database. Those functions can then be exposed in the api via Convex functions, or more than one can be used in a function. The plain functions become composable building blocks. In those functions I also include shared logic to enforce authz. I can say a lot about the patterns I'ver personally landed on but there are a number of ways folks have approached this. It helps to understand that Convex is primarily a reactive database core. The team is actively working on building up supporting features and ergonomics, but it's still early days in that regard. So it's all about patterns.

Despite some of the work it takes to build up those patterns, the benefits of Convex have massively outweigh the presence of mind required, imo.
igor9silva
igor9silvaOPβ€’2mo ago
if able to just write stuff like that
export const find = query({
args: {
taskId: v.id('tasks'),
},
handler: async (ctx, { taskId }) => {
// ... db logic
},
});
export const find = query({
args: {
taskId: v.id('tasks'),
},
handler: async (ctx, { taskId }) => {
// ... db logic
},
});
and call find() from other queries/mutations + ctx.runQuery(api.tasks.find) from actions it'd just work for any use case, only thing to wonder would be "public or internal?" but since I have to call the handler fn directly from queries/mutations, for every new piece I'll have to wonder "do I need to call it from an action?" + "do I need to call it from a mutation/query?" and not just for new stuff, because I'll eventually need it from a mutation where I previously did not it also cascades into other minor concerns such as: - naming conventioning, as I'll have up to 2 variables for the same thing - typing, as just implementing the handler will perfectly infer every param type :chef_kiss: that was an amazing comment i'll go with that approach for now, always writing pure functions and then wrapping them as needed but nevertheless it's great to hear that "the team has acknowledged it'd be ideal if queries and mutations could call one another without overhead", it would be awesome
It helps to understand that Convex is primarily a reactive database core. The team is actively working on building up supporting features and ergonomics, but it's still early days in that regard.
that was also very nice to learn I have not been following Convex for long, and that's not clear or easily assumed I've been (deeply) prototyping with a bunch of different web stacks for the last few months (I've tried the same use case with MongoDB Atlas, Upstash, Redis, RabbitMQ, G Pub/Sub, NATS), with a bunch of different architectures, and I must say I'm very impressed by Convex so far it fills SO many boxes and, most importantly, fits my personal engineering vision so deeply that it's been hard to believe it's a real product I'm a long-time MongoDB (+Atlas) user and fan, but it's been painfully hard in the "ergonomics" aspect you mentioned (e.g. I may have spent a total of 20 hours at least fighting the ObjectId type on TS), and Convex feels like what I always wanted MongoDB to be so even though you guys are "primarily a reactive database core", you've been kicking ass in the ergonomics area already! DX been amazing so far the query/mutation/action abstraction is so beautiful - truly the building blocks I've been looking for oh, and you guys being TanStack sponsors?!?! wildly amazing, I'm a monumental fan of Tanner and his work (that also implies you'll be supporting their stuff very quickly, which already does look like with Start 😁) and the energy here... I've written maybe 3 times here in the last few days, and have been replied with deep and meaningful answers within minutes, even from the CEO I'm honestly rooting for you guys, you're definitely on the right track!
Gustavo
Gustavoβ€’2mo ago
Just wanted to say I agree with @igor9silva for these exact reasons. The less I have to worry about naming and typing, the happier I'm. If calling find() directly can cause problems, something else like find.run() would already help. Better than having to split into a separate function.
erquhart
erquhartβ€’2mo ago
Note, the question of β€œdo I need to call it from an action/query/mutation is always answered by the typescript compiler Also, just to clarify, I’m just a community member like you. And I 100% feel the things you said above.
igor9silva
igor9silvaOPβ€’2mo ago
having deeply engaged community is also +1️⃣ I mean like my use case "do I need it from an action?"
igor9silva
igor9silvaOPβ€’2mo ago
I also have to create Types now 😒
No description
igor9silva
igor9silvaOPβ€’2mo ago
I think I'll just ignore performance for a while
deen
deenβ€’2mo ago
You can adopt these techniques incrementally. Focus on building your project first - it's the best way to learn IMO Create your validators outside of the convex function, import type { Infer } from 'convex/values' - infer the type. Now you can share the type with your typescript functions, and the validator with other convex functions. You can even destructure the validator to select just the parts you want, if you need to.

Did you find this page helpful?