anti db delete

This commit is contained in:
2026-07-14 23:40:54 -04:00
parent 11fa5ec4ee
commit 3ac6ad1f2f
+102 -23
View File
@@ -60,6 +60,13 @@ function isUserStarred(userId: number, ratingId: number, categoryId: number) {
let cacheLoaded = false;
const pendingUserVoteChanges = new Map<string, { userId: number; ratingId: number; ratingCategoryId: number; score: number }>();
const pendingUserVoteDeletes = new Set<string>();
const pendingAnonVoteChanges = new Map<string, { ip: string; ratingId: number; ratingCategoryId: number; score: number }>();
const pendingAnonVoteDeletes = new Set<string>();
const pendingStarChanges = new Map<string, { userId: number; ratingId: number; ratingCategoryId: number }>();
const pendingStarDeletes = new Set<string>();
function getUserVotesForRating(userId: number, ratingId: number): Record<number, number> {
const result: Record<number, number> = {};
const perRating = cache.userVotes.get(userId)?.get(ratingId);
@@ -152,32 +159,86 @@ async function rebuildCache() {
async function flushPendingOps() {
if (!cacheLoaded) return;
const userRatingData: { userId: number; ratingId: number; ratingCategoryId: number; score: number }[] = [];
for (const [userId, perRating] of cache.userVotes) {
for (const [ratingId, perCat] of perRating) {
for (const [catId, score] of perCat) userRatingData.push({ userId, ratingId, ratingCategoryId: catId, score });
}
}
const anonVoteData: { ip: string; ratingId: number; ratingCategoryId: number; score: number }[] = [];
for (const [ip, perRating] of cache.anonVotes) {
for (const [ratingId, perCat] of perRating) {
for (const [catId, score] of perCat) anonVoteData.push({ ip, ratingId, ratingCategoryId: catId, score });
}
}
const starData: { userId: number; ratingId: number; ratingCategoryId: number }[] = [];
for (const [userId, perRating] of cache.userStars) {
for (const [ratingId, cats] of perRating) {
for (const catId of cats) starData.push({ userId, ratingId, ratingCategoryId: catId });
}
const uvChanges = new Map(pendingUserVoteChanges);
const uvDeletes = new Set(pendingUserVoteDeletes);
const avChanges = new Map(pendingAnonVoteChanges);
const avDeletes = new Set(pendingAnonVoteDeletes);
const stChanges = new Map(pendingStarChanges);
const stDeletes = new Set(pendingStarDeletes);
pendingUserVoteChanges.clear();
pendingUserVoteDeletes.clear();
pendingAnonVoteChanges.clear();
pendingAnonVoteDeletes.clear();
pendingStarChanges.clear();
pendingStarDeletes.clear();
if (uvChanges.size === 0 && uvDeletes.size === 0 &&
avChanges.size === 0 && avDeletes.size === 0 &&
stChanges.size === 0 && stDeletes.size === 0) {
return;
}
await prisma.$transaction(async (tx) => {
await tx.userRating.deleteMany();
await tx.anonymousRatingVote.deleteMany();
await tx.ratingStar.deleteMany();
if (userRatingData.length) await tx.userRating.createMany({ data: userRatingData });
if (anonVoteData.length) await tx.anonymousRatingVote.createMany({ data: anonVoteData });
if (starData.length) await tx.ratingStar.createMany({ data: starData });
for (const [, v] of uvChanges) {
await tx.userRating.upsert({
where: { userId_ratingId_ratingCategoryId: { userId: v.userId, ratingId: v.ratingId, ratingCategoryId: v.ratingCategoryId } },
create: { userId: v.userId, ratingId: v.ratingId, ratingCategoryId: v.ratingCategoryId, score: v.score },
update: { score: v.score },
});
}
for (const key of uvDeletes) {
const [userId, ratingId, ratingCategoryId] = key.split(":").map(Number);
await tx.userRating.deleteMany({ where: { userId, ratingId, ratingCategoryId } });
}
for (const [, v] of avChanges) {
await tx.anonymousRatingVote.upsert({
where: { ip_ratingId_ratingCategoryId: { ip: v.ip, ratingId: v.ratingId, ratingCategoryId: v.ratingCategoryId } },
create: { ip: v.ip, ratingId: v.ratingId, ratingCategoryId: v.ratingCategoryId, score: v.score },
update: { score: v.score },
});
}
for (const key of avDeletes) {
const [ip, ratingId, ratingCategoryId] = key.split(":");
await tx.anonymousRatingVote.deleteMany({ where: { ip, ratingId: Number(ratingId), ratingCategoryId: Number(ratingCategoryId) } });
}
for (const [, v] of stChanges) {
await tx.ratingStar.upsert({
where: { userId_ratingId_ratingCategoryId: { userId: v.userId, ratingId: v.ratingId, ratingCategoryId: v.ratingCategoryId } },
create: { userId: v.userId, ratingId: v.ratingId, ratingCategoryId: v.ratingCategoryId },
update: {},
});
}
for (const key of stDeletes) {
const [userId, ratingId, ratingCategoryId] = key.split(":").map(Number);
await tx.ratingStar.deleteMany({ where: { userId, ratingId, ratingCategoryId } });
}
});
const [allVotes, anonVotes, allStars] = await Promise.all([
prisma.userRating.findMany(),
prisma.anonymousRatingVote.findMany(),
prisma.ratingStar.findMany(),
]);
cache.userVotes.clear();
for (const v of allVotes) {
let u = cache.userVotes.get(v.userId); if (!u) { u = new Map(); cache.userVotes.set(v.userId, u); }
let r = u.get(v.ratingId); if (!r) { r = new Map(); u.set(v.ratingId, r); }
r.set(v.ratingCategoryId, v.score);
}
cache.anonVotes.clear();
for (const v of anonVotes) {
let a = cache.anonVotes.get(v.ip); if (!a) { a = new Map(); cache.anonVotes.set(v.ip, a); }
let r = a.get(v.ratingId); if (!r) { r = new Map(); a.set(v.ratingId, r); }
r.set(v.ratingCategoryId, v.score);
}
cache.userStars.clear();
for (const s of allStars) {
let u = cache.userStars.get(s.userId); if (!u) { u = new Map(); cache.userStars.set(s.userId, u); }
let r = u.get(s.ratingId); if (!r) { r = new Set(); u.set(s.ratingId, r); }
r.add(s.ratingCategoryId);
}
}
const uploadsDir = path.join(__dirname, "../uploads");
@@ -509,12 +570,18 @@ app.post("/api/star", async (req, res) => {
if (!cats) { cats = new Set(); perUser.set(ratingId, cats); }
if (cats.has(catId)) {
cats.delete(catId);
const key = `${userId}:${ratingId}:${catId}`;
pendingStarDeletes.add(key);
pendingStarChanges.delete(key);
res.json({ starred: false });
} else {
let catCount = 0;
for (const sci of perUser.values()) { if (sci.has(catId)) catCount++; }
if (catCount >= 3) { res.status(400).json({ error: "Max 3 stars per category" }); return; }
cats.add(catId);
const key = `${userId}:${ratingId}:${catId}`;
pendingStarChanges.set(key, { userId, ratingId, ratingCategoryId: catId });
pendingStarDeletes.delete(key);
res.json({ starred: true });
}
});
@@ -547,11 +614,17 @@ app.post("/api/vote", async (req, res) => {
let u = cache.userVotes.get(userId); if (!u) { u = new Map(); cache.userVotes.set(userId, u); }
let r = u.get(ratingId); if (!r) { r = new Map(); u.set(ratingId, r); }
r.set(ratingCategoryId, score);
const key = `${userId}:${ratingId}:${ratingCategoryId}`;
pendingUserVoteChanges.set(key, { userId, ratingId, ratingCategoryId, score });
pendingUserVoteDeletes.delete(key);
} else {
const ip = getIp(req);
let a = cache.anonVotes.get(ip); if (!a) { a = new Map(); cache.anonVotes.set(ip, a); }
let r = a.get(ratingId); if (!r) { r = new Map(); a.set(ratingId, r); }
r.set(ratingCategoryId, score);
const key = `${ip}:${ratingId}:${ratingCategoryId}`;
pendingAnonVoteChanges.set(key, { ip, ratingId, ratingCategoryId, score });
pendingAnonVoteDeletes.delete(key);
}
res.json(getAvgAndTotal(ratingId, ratingCategoryId));
});
@@ -566,9 +639,15 @@ app.post("/api/unvote", async (req, res) => {
}
if (userId) {
cache.userVotes.get(userId)?.get(ratingId)?.delete(ratingCategoryId);
const key = `${userId}:${ratingId}:${ratingCategoryId}`;
pendingUserVoteDeletes.add(key);
pendingUserVoteChanges.delete(key);
} else {
const ip = getIp(req);
cache.anonVotes.get(ip)?.get(ratingId)?.delete(ratingCategoryId);
const key = `${ip}:${ratingId}:${ratingCategoryId}`;
pendingAnonVoteDeletes.add(key);
pendingAnonVoteChanges.delete(key);
}
res.json(getAvgAndTotal(ratingId, ratingCategoryId));
});