-
-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Add database transactions where necessary #391
base: main
Are you sure you want to change the base?
Conversation
orderBy: { | ||
date: 'desc', | ||
}, | ||
include: { | ||
sources: true, | ||
}, | ||
}); | ||
} as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: Necessary to prevent type widening.
For example, fi I didn't use as const
, TS would infer the type of statsQuery.orderBy.date
as string
, which is to permissive to be accepted by findFirst
and findMany
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. Why do that instead of directly typing it?
@@ -35,11 +35,15 @@ export const generateGptResponse: GenerateGptResponse<GenerateGptResponseInput, | |||
context | |||
) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This operation went through many changes.
It's not 100% foolproof (i.e., doesn't cover all the edge cases).
It can be done, but I can't do it in a way that's simple enough to make sense for an OpenSaas example app. I believe I'd need an extra table with pending responses and a job that periodically deletes unfinished responses and refunds the user.
In any case, I'm not merging this before a call with Vinny since I don't fully understand the old code. I left a bunch of TODO
s and NOTE
s with my questions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about packing everyting into the transaction itself (by doing this do we lose Wasp's automatic cache invalidation) ?
const eligiblityCheckPrismaTx = await prisma.$transaction(async (tx) => {
const freshUserData = await tx.user.findUnique({
where: { id: context.user!.id },
});
if (!freshUserData) {
throw new HttpError(404, 'User not found');
}
if (!isEligibleForResponse(freshUserData)) {
throw new HttpError(402, 'User has not paid or is out of credits');
}
if (!userHasSubscriptionPlanEffect(freshUserData)) {
const updatedUser = await tx.user.update({
where: { id: context.user!.id },
data: { credits: { decrement: 1 } },
});
}
const gptResponse = await tx.gptResponse.create({
data: {
user: { connect: { id: context.user!.id } },
content: dailyPlanJson,
},
});
return { updatedUser, gptResponse };
});
description, | ||
time, | ||
})); | ||
console.log('Calling open AI api'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leftover log
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I included this one on purpose 😄
We already had "Decrementing credits," so it seemed fitting, especially since the "decrementing credits" message now appears very late (after we get the response from GPT).
@@ -296,3 +211,98 @@ export const getAllTasksByUser: GetAllTasksByUser<void, Task[]> = async (_args, | |||
}); | |||
}; | |||
//#endregion | |||
|
|||
// TODO: Why is hours a string? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be easily be changed, update the operations validation, parse the input
as number and change it here as well. I guess it was easier to just send the string
that was received from input
directly.
|
||
// TODO: Why is hours a string? | ||
async function getDailyPlanFromGpt(tasks: Task[], hours: string): Promise<string | null> { | ||
// TODO: Why was this a singleton |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's like creating an instance of axios
and using it everywhere. We init the OpenAI client and use it in multiple places. In some projects we would even put it in openai.ts
and import it as a service we use. I'd keep it that way, since most users are used to that kind of usage.
// NOTE: I changed this up, first I do the request, and then I decrement | ||
// credits. Is that dangerous? Protecting from it could be too complicated | ||
// TODO: Potential issue here, user can lose credits between the point we | ||
// inject it into the context and here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand what you mean here by "user can lose credits between the point we inject it into context and here".
But, yes, I see the problem. When using prisma transactions we have to run them at the end after the openai request, and I think that's fine.
// credits. Is that dangerous? Protecting from it could be too complicated | ||
// TODO: Potential issue here, user can lose credits between the point we | ||
// inject it into the context and here | ||
// Seems to me that there should be users in the database with negative credits |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where: {
id: user.id,
credits: { gte: 1 },
},
This could solve that issue, but I've also suggested another approach which is putting the eligibility check within the transaction itself (see my other comment)
console.log('gpt function call arguments: ', gptArgs); | ||
// NOTE: Since these two are now brought together, I put them in a single transaction - so no rollback necessary | ||
// TODO: But what if the server crashes after the transaction and before | ||
// the response? I guess no big deal, since the response is in the db. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
throw new HttpError(statusCode, errorMessage); | ||
} | ||
// TODO: Can this ever fail? | ||
return JSON.parse(dailyPlanJson); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's an invalid json string then yes it can. We could check it first to make sure it is before parsing perhaps.
// TODO: Why not check for allowed states? | ||
const isUserSubscribed = | ||
user.subscriptionStatus !== SubscriptionStatus.Deleted && | ||
user.subscriptionStatus !== SubscriptionStatus.PastDue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assumed that it's easier to check for two unapproved states rather than more allowed states (the app could add more allowed states like trialing
, for example). What's best practice?
// shouldn't take priority over credits since it makes no sece spending | ||
// credits when a subscription is active? | ||
// If they aren't subscribed, then it would make sense to spend credits. | ||
// The old code always subtracted credits so I kept that, is this a bug? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it is. We could add a function like returnPaymentPlanEffect
and if it's subscription
then we don't pass the credit decrement user update.
orderBy: { | ||
date: 'desc', | ||
}, | ||
include: { | ||
sources: true, | ||
}, | ||
}); | ||
} as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. Why do that instead of directly typing it?
@@ -35,11 +35,15 @@ export const generateGptResponse: GenerateGptResponse<GenerateGptResponseInput, | |||
context | |||
) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about packing everyting into the transaction itself (by doing this do we lose Wasp's automatic cache invalidation) ?
const eligiblityCheckPrismaTx = await prisma.$transaction(async (tx) => {
const freshUserData = await tx.user.findUnique({
where: { id: context.user!.id },
});
if (!freshUserData) {
throw new HttpError(404, 'User not found');
}
if (!isEligibleForResponse(freshUserData)) {
throw new HttpError(402, 'User has not paid or is out of credits');
}
if (!userHasSubscriptionPlanEffect(freshUserData)) {
const updatedUser = await tx.user.update({
where: { id: context.user!.id },
data: { credits: { decrement: 1 } },
});
}
const gptResponse = await tx.gptResponse.create({
data: {
user: { connect: { id: context.user!.id } },
content: dailyPlanJson,
},
});
return { updatedUser, gptResponse };
});
No description provided.