fix: avoid restamping deletedAt + skip Redis DEL for already-soft-deleted slugs

Address Greptile review feedback on #4243.

- Cloud-mode link.updateMany / pixel.updateMany now filter where: { ..., deletedAt: null } so a previously soft-deleted row keeps its original deletion timestamp instead of being restamped with the current time.
- Pre-transaction findMany now selects deletedAt; the Redis invalidation list filters to only live slugs, avoiding harmless but wasted DEL calls for already-soft-deleted entries.

Note: the share.deleteMany cleanup still uses the broad entityId list (not filtered by deletedAt) so that orphan share rows of already-soft-deleted links/pixels are still cleaned up. Filtering the prefetch itself, as Greptile's exact suggestion proposed, would skip those shares while link.deleteMany still hard-deletes the rows, leaving orphan share rows behind. Verified empirically with a 3-scenario reproduction.
This commit is contained in:
Stanislaw
2026-05-07 04:14:38 +02:00
parent a57abbe039
commit 71ee000f21
2 changed files with 40 additions and 12 deletions
+20 -6
View File
@@ -146,13 +146,20 @@ export async function deleteTeam(teamId: string) {
const cloudMode = !!process.env.CLOUD_MODE;
const [links, pixels, boards] = await Promise.all([
client.link.findMany({ where: { teamId }, select: { id: true, slug: true } }),
client.pixel.findMany({ where: { teamId }, select: { id: true, slug: true } }),
client.link.findMany({
where: { teamId },
select: { id: true, slug: true, deletedAt: true },
}),
client.pixel.findMany({
where: { teamId },
select: { id: true, slug: true, deletedAt: true },
}),
client.board.findMany({ where: { teamId }, select: { id: true } }),
]);
const entityIds = [...links.map(l => l.id), ...pixels.map(p => p.id), ...boards.map(b => b.id)];
const linkSlugs = links.map(l => l.slug);
const pixelSlugs = pixels.map(p => p.slug);
// Only invalidate Redis cache for slugs that are still live (not already soft-deleted).
const linkSlugs = links.filter(l => !l.deletedAt).map(l => l.slug);
const pixelSlugs = pixels.filter(p => !p.deletedAt).map(p => p.slug);
const invalidateRedis = async () => {
if (redis.enabled && (linkSlugs.length || pixelSlugs.length)) {
@@ -174,8 +181,15 @@ export async function deleteTeam(teamId: string) {
},
}),
client.share.deleteMany({ where: { entityId: { in: entityIds } } }),
client.link.updateMany({ data: { deletedAt: new Date() }, where: { teamId } }),
client.pixel.updateMany({ data: { deletedAt: new Date() }, where: { teamId } }),
// deletedAt: null avoids restamping rows that were already soft-deleted earlier.
client.link.updateMany({
data: { deletedAt: new Date() },
where: { teamId, deletedAt: null },
}),
client.pixel.updateMany({
data: { deletedAt: new Date() },
where: { teamId, deletedAt: null },
}),
client.board.deleteMany({ where: { teamId } }),
]).then(async result => {
await invalidateRedis();
+20 -6
View File
@@ -135,13 +135,20 @@ export async function deleteUser(userId: string) {
: { OR: [{ userId }, { teamId: { in: teamIds } }] };
const [links, pixels, boards] = await Promise.all([
client.link.findMany({ where: ownedFilter, select: { id: true, slug: true } }),
client.pixel.findMany({ where: ownedFilter, select: { id: true, slug: true } }),
client.link.findMany({
where: ownedFilter,
select: { id: true, slug: true, deletedAt: true },
}),
client.pixel.findMany({
where: ownedFilter,
select: { id: true, slug: true, deletedAt: true },
}),
client.board.findMany({ where: ownedFilter, select: { id: true } }),
]);
const entityIds = [...links.map(l => l.id), ...pixels.map(p => p.id), ...boards.map(b => b.id)];
const linkSlugs = links.map(l => l.slug);
const pixelSlugs = pixels.map(p => p.slug);
// Only invalidate Redis cache for slugs that are still live (not already soft-deleted).
const linkSlugs = links.filter(l => !l.deletedAt).map(l => l.slug);
const pixelSlugs = pixels.filter(p => !p.deletedAt).map(p => p.slug);
const invalidateRedis = async () => {
if (redis.enabled && (linkSlugs.length || pixelSlugs.length)) {
@@ -170,8 +177,15 @@ export async function deleteUser(userId: string) {
},
}),
client.share.deleteMany({ where: { entityId: { in: entityIds } } }),
client.link.updateMany({ data: { deletedAt: new Date() }, where: { userId } }),
client.pixel.updateMany({ data: { deletedAt: new Date() }, where: { userId } }),
// deletedAt: null avoids restamping rows that were already soft-deleted earlier.
client.link.updateMany({
data: { deletedAt: new Date() },
where: { userId, deletedAt: null },
}),
client.pixel.updateMany({
data: { deletedAt: new Date() },
where: { userId, deletedAt: null },
}),
client.board.deleteMany({ where: { userId } }),
]).then(async result => {
await invalidateRedis();