Update features and dependencies
This commit is contained in:
@@ -16,6 +16,60 @@ export class TierLimitError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically checks the tier limit and increments the usage counter in a
|
||||
* single SQL statement, eliminating the TOCTOU race that existed when
|
||||
* checkTierLimit and incrementUsage were called separately.
|
||||
*
|
||||
* Throws TierLimitError if the limit has already been reached.
|
||||
* Use this instead of the separate checkTierLimit + incrementUsage pair
|
||||
* for "recipe" and "aiCall" keys.
|
||||
*/
|
||||
export async function checkAndIncrementTierLimit(
|
||||
userId: string,
|
||||
userTier: "free" | "pro",
|
||||
key: "recipe" | "aiCall"
|
||||
): Promise<void> {
|
||||
const [tierDef] = await db
|
||||
.select()
|
||||
.from(tierDefinitions)
|
||||
.where(eq(tierDefinitions.tier, userTier));
|
||||
|
||||
if (!tierDef) return;
|
||||
|
||||
const month = currentMonth();
|
||||
const id = `${userId}-${month}`;
|
||||
|
||||
if (key === "aiCall") {
|
||||
const limit = tierDef.aiCallsPerMonth;
|
||||
const result = await db.execute(sql`
|
||||
INSERT INTO user_usage (id, user_id, month, ai_calls_used, recipe_count, storage_used_mb)
|
||||
VALUES (${id}, ${userId}, ${month}, 1, 0, 0)
|
||||
ON CONFLICT (user_id, month) DO UPDATE
|
||||
SET ai_calls_used = user_usage.ai_calls_used + 1
|
||||
WHERE user_usage.ai_calls_used < ${limit}
|
||||
RETURNING ai_calls_used
|
||||
`);
|
||||
if (result.length === 0) {
|
||||
throw new TierLimitError("aiCall", userTier);
|
||||
}
|
||||
} else {
|
||||
const limit = tierDef.maxRecipes;
|
||||
const result = await db.execute(sql`
|
||||
INSERT INTO user_usage (id, user_id, month, ai_calls_used, recipe_count, storage_used_mb)
|
||||
VALUES (${id}, ${userId}, ${month}, 0, 1, 0)
|
||||
ON CONFLICT (user_id, month) DO UPDATE
|
||||
SET recipe_count = user_usage.recipe_count + 1
|
||||
WHERE user_usage.recipe_count < ${limit}
|
||||
RETURNING recipe_count
|
||||
`);
|
||||
if (result.length === 0) {
|
||||
throw new TierLimitError("recipe", userTier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use checkAndIncrementTierLimit for recipe/aiCall keys to avoid TOCTOU races. */
|
||||
export async function checkTierLimit(
|
||||
userId: string,
|
||||
userTier: "free" | "pro",
|
||||
|
||||
Reference in New Issue
Block a user