fix: revoke points when a comment is deleted
All checks were successful
Build and Push Backend Image / build (push) Successful in 43s
All checks were successful
Build and Push Backend Image / build (push) Successful in 43s
- revokePoints now accepts optional recordingId; when absent it deletes one matching row (for actions like COMMENT_CREATE that have no recording) - deleteComment queues revokePoints + checkAchievements so leaderboard and social achievements stay in sync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,18 @@ builder.mutationField("deleteComment", (t) =>
|
||||
if (!comment[0]) throw new GraphQLError("Comment not found");
|
||||
requireOwnerOrAdmin(ctx, comment[0].user_id);
|
||||
await ctx.db.delete(comments).where(eq(comments.id, args.id));
|
||||
|
||||
await gamificationQueue.add("revokePoints", {
|
||||
job: "revokePoints",
|
||||
userId: comment[0].user_id,
|
||||
action: "COMMENT_CREATE",
|
||||
});
|
||||
await gamificationQueue.add("checkAchievements", {
|
||||
job: "checkAchievements",
|
||||
userId: comment[0].user_id,
|
||||
category: "social",
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { eq, sql, and, gt, isNotNull, count, sum } from "drizzle-orm";
|
||||
import { eq, sql, and, gt, isNull, isNotNull, count, sum } from "drizzle-orm";
|
||||
import type { DB } from "../db/connection";
|
||||
import {
|
||||
user_points,
|
||||
@@ -45,17 +45,33 @@ export async function revokePoints(
|
||||
db: DB,
|
||||
userId: string,
|
||||
action: keyof typeof POINT_VALUES,
|
||||
recordingId: string,
|
||||
recordingId?: string,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.delete(user_points)
|
||||
.where(
|
||||
and(
|
||||
eq(user_points.user_id, userId),
|
||||
eq(user_points.action, action),
|
||||
eq(user_points.recording_id, recordingId),
|
||||
),
|
||||
);
|
||||
const recordingCondition = recordingId
|
||||
? eq(user_points.recording_id, recordingId)
|
||||
: isNull(user_points.recording_id);
|
||||
|
||||
// When no recordingId (e.g. COMMENT_CREATE), delete only one row so each
|
||||
// revoke undoes exactly one prior award.
|
||||
if (!recordingId) {
|
||||
const row = await db
|
||||
.select({ id: user_points.id })
|
||||
.from(user_points)
|
||||
.where(
|
||||
and(eq(user_points.user_id, userId), eq(user_points.action, action), recordingCondition),
|
||||
)
|
||||
.limit(1);
|
||||
if (row[0]) {
|
||||
await db.delete(user_points).where(eq(user_points.id, row[0].id));
|
||||
}
|
||||
} else {
|
||||
await db
|
||||
.delete(user_points)
|
||||
.where(
|
||||
and(eq(user_points.user_id, userId), eq(user_points.action, action), recordingCondition),
|
||||
);
|
||||
}
|
||||
|
||||
await updateUserStats(db, userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const log = logger.child({ component: "gamification-worker" });
|
||||
|
||||
export type GamificationJobData =
|
||||
| { job: "awardPoints"; userId: string; action: keyof typeof POINT_VALUES; recordingId?: string }
|
||||
| { job: "revokePoints"; userId: string; action: keyof typeof POINT_VALUES; recordingId: string }
|
||||
| { job: "revokePoints"; userId: string; action: keyof typeof POINT_VALUES; recordingId?: string }
|
||||
| { job: "checkAchievements"; userId: string; category?: string };
|
||||
|
||||
export function startGamificationWorker(): Worker {
|
||||
|
||||
Reference in New Issue
Block a user