-
-
Notifications
You must be signed in to change notification settings - Fork 75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature Request] Auto-generated Transactions API endpoint #1203
Comments
Hi @tmax22 , I'm glad you found ZenStack helpful and really appreciate your recognition! I think a transaction hook will be very useful. The main challenge today is that on the backend side, ZenStack-enhanced PrismaClient doesn't support batch transactions yet (interactive ones are supported). But I want to revisit the limitation and see if it's resolveable now with refactors done in V2. If that's added, the sort of transaction you proposed should be easier to implement. Since you said "our real use case is more complicated, with updates being dependent on the creation of previous records.", does it mean that you'll likely need interactive transactions on the hooks side as well? |
Indeed, our use case necessitates the use of interactive transactions due to the requirement of referencing a recordId that is generated in a subsequent operation. However, my initial example was overly simplistic and only included read operations. I'm uncertain about the correct approach to handle mutations, those return handlers that must be invoked, as opposed to returning the data object itself. Furthermore, I find it challenging to conceptualize the appropriate way to define the client API for client-side interactive transactions. Let's imagine client-side Interactive transactionlooking at the Prisma interactive // backend example
import {PrismaClient} from '@prisma/client'
const prisma = new PrismaClient()
function transfer(from: string, to: string, amount: number) {
return prisma.$transaction(async (tx) => {
// 1. Decrement amount from the sender.
const sender = await tx.account.update({
data: {
balance: {
decrement: amount,
},
},
where: {
email: from,
},
})
// 2. Verify that the sender's balance didn't go below zero.
if (sender.balance < 0) {
throw new Error(`${from} doesn't have enough to send ${amount}`)
}
// 3. Increment the recipient's balance by amount
const recipient = await tx.account.update({
data: {
balance: {
increment: amount,
},
},
where: {
email: to,
},
})
return recipient
})
}
async function main() {
// This transfer is successful
await transfer('alice@prisma.io', 'bob@prisma.io', 100)
// This transfer fails because Alice doesn't have enough funds in her account
await transfer('alice@prisma.io', 'bob@prisma.io', 100)
}
main() how would you call transfer-like operation from the client-side? we can imagine // client example
const MyComponent = () => {
const transaction = useTransaction()
const handleTransfer = async () => {
try {
await transaction(async (tx) => {
// 1. Decrement amount from the sender.
const sender = await tx.account.update({
data: {
balance: {
decrement: amount,
},
},
where: {
email: from,
},
})
// 2. Verify that the sender's balance didn't go below zero.
if (sender.balance < 0) {
throw new Error(`${from} doesn't have enough to send ${amount}`)
}
// 3. Increment the recipient's balance by amount
const recipient = await tx.account.update({
data: {
balance: {
increment: amount,
},
},
where: {
email: to,
}
})
return recipient
})
} catch
(e) {
console.error(e)
}
}
return (
<button onClick={handleTransfer}>Transfer</button>
)
} One potential strategy involves initiating a new interactive transaction on the server when the The Each method invocation on The Once the asynchronous function passed to While using a WebSocket might pose challenges when deploying on serverless platforms, it could be a viable approach in this case. The WebSocket session would only remain open for the duration of the transaction, and would be closed once the asynchronous function finishes execution. Implementing this approach would certainly be complex and would require further research to make the best design decisions. However, it appears to be a feasible solution for handling interactive transactions from the client side. Let me know your thoughts, thanks! |
Hi @tmax22 , sorry for the late response, and thanks for the detailed elaboration of your thoughts! I agree having interactive transactions in the frontend will be really cool (with a magical feeling too 😄). I see we'll have to use WebSocket to proxy back and forth. My main concern is this can potentially leave a server-side transaction hanging for a long time due to connection interruption, which can in turn cause unexpected database performance degradation or locking. I'm wondering if it's simpler to just implement the logic as an API router or server action (if you're using Next.js) and use the ZenStack-enhanced PrismaClient in it for access control. Is it feasible for you? I understand there's no strong-typed frontend hooks support for this approach, but maybe we can continue exploring some options along this route if it sounds sensible. |
what do you mean by API router? your concern about server-side transactions hanging for a long time due to connection interruption can be handled by some kind of timeout limit. I'm not saying my WebSocket implementation suggestion should be the best one, and maybe there are better approaches. |
Sorry, I meant to say API route (or something equivalent if you don't use Next.js). I'm basically thinking whether a complex transaction should be better contained in a real backend routine instead of implemented from the frontend. I guess you've probably already considered such an alternative, but I'd like to understand your considerations, |
hey @ymc9 I want to play around with this idea and check out how feasible it is. I've cloned zenstack monorepo and i have a demo of zenstack app (backend+frontend in separate packages). |
Sorry that I missed this comment @Eliav2 . For full-stack experiments, I usually have a standalone project aside and copy over updated js files or make a symlink. I think it's a good idea to have a set of demo packages in the repo for both experimenting and learning. I'll find some time to try it out. There's a very simple contributing guide here, which only covers the basics: https://github.com/zenstackhq/zenstack/blob/main/CONTRIBUTING.md Do you plan to implement a transaction API at the server adapter layer? I'm very interested in knowing more about your thoughts. Thanks! |
I've already taken the approach of a "nested" pnpm workspace with a demo project containing git sub module for my zenstack fork, and including zenstack workspaces in the upper demo workspace. It provides awesome development environment and immidiate reflection of changes in zenstack(besides typescript which causes me some problems just yet). You can check it out here. I haven't had the time to seriously implement it yet, by my approach is to add another endpoint to the zenstack server adapter at /transaction that starts a websocket connection and wait for transaction operations, and generate react query hook which responsible to initiate this socket and passing requests through this socket and terminating the connection at the end of async handler passed at the client side. I would update when I would have time to work on it. I would also would love to hear your thoughts ! |
Nice, the demo structure looks pretty cool! Got it. I still have the worry that we'll probably allow the frontend to do "too much" 😄. Besides the risk of leaving dangling transactions, I feel non-trivial CRUD is also traditionally considered as backend implementation details, which maybe people often won't feel comfortable to leak to the frontend code? I may be too conservative though. Alternatively, what do you think about introducing something like "stored procedures"? Basically a function declared in ZModel (so hooks generator knows it), and the implementation registered to the server adapter (so it becomes part of the auto CRUD API). If you use Supabase, the idea is close to its edge functions. |
Btw, for your use case, have you considered using trpc to achieve implementing a backend transaction and call it directly from the frontend? |
Zenstack already supports automatic CRUD API with generated react-query hooks(which is insane, thanks!), so for example for Post model:
we can query for Posts from the client using the generated react-query hooks such
useFindManyPost()
my request is a special hook, to a special endpoint
/mode/$transaction
on the generated CRUD API, that would allow a special react-query hookuseTransaction
.The usage could be similar to:
which would be directly translated to
our real use case is more complicated, with updates being dependent on the creation of previous records.
let me know what you thinks. and again, thank you for truly awsome framework!
The text was updated successfully, but these errors were encountered: