Merge pull request #4243 from anvme/fix/delete-cascade
fix: clean up link, pixel, board rows on user/team deletion
This commit is contained in:
@@ -21,6 +21,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
|
|||||||
return findPixel({
|
return findPixel({
|
||||||
where: {
|
where: {
|
||||||
slug,
|
slug,
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -34,6 +35,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
|
|||||||
pixel = await findPixel({
|
pixel = await findPixel({
|
||||||
where: {
|
where: {
|
||||||
slug,
|
slug,
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
|
|||||||
return findLink({
|
return findLink({
|
||||||
where: {
|
where: {
|
||||||
slug,
|
slug,
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -32,6 +33,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
|
|||||||
link = await findLink({
|
link = await findLink({
|
||||||
where: {
|
where: {
|
||||||
slug,
|
slug,
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Prisma, type Team } from '@/generated/prisma/client';
|
|||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
|
import redis from '@/lib/redis';
|
||||||
import { sanitizeSortFilters } from '@/lib/sort';
|
import { sanitizeSortFilters } from '@/lib/sort';
|
||||||
import type { PageResult, QueryFilters } from '@/lib/types';
|
import type { PageResult, QueryFilters } from '@/lib/types';
|
||||||
|
|
||||||
@@ -148,6 +149,31 @@ export async function deleteTeam(teamId: string) {
|
|||||||
const { client, transaction } = prisma;
|
const { client, transaction } = prisma;
|
||||||
const cloudMode = !!process.env.CLOUD_MODE;
|
const cloudMode = !!process.env.CLOUD_MODE;
|
||||||
|
|
||||||
|
const [links, pixels, boards] = await Promise.all([
|
||||||
|
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)];
|
||||||
|
// 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)) {
|
||||||
|
await Promise.all([
|
||||||
|
...linkSlugs.map(slug => redis.client.del(`link:${slug}`)),
|
||||||
|
...pixelSlugs.map(slug => redis.client.del(`pixel:${slug}`)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (cloudMode) {
|
if (cloudMode) {
|
||||||
return transaction([
|
return transaction([
|
||||||
client.team.update({
|
client.team.update({
|
||||||
@@ -158,7 +184,21 @@ export async function deleteTeam(teamId: string) {
|
|||||||
id: teamId,
|
id: teamId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
client.share.deleteMany({ where: { entityId: { in: entityIds } } }),
|
||||||
|
// 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();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return transaction([
|
return transaction([
|
||||||
@@ -167,10 +207,17 @@ export async function deleteTeam(teamId: string) {
|
|||||||
teamId,
|
teamId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
client.share.deleteMany({ where: { entityId: { in: entityIds } } }),
|
||||||
|
client.link.deleteMany({ where: { teamId } }),
|
||||||
|
client.pixel.deleteMany({ where: { teamId } }),
|
||||||
|
client.board.deleteMany({ where: { teamId } }),
|
||||||
client.team.delete({
|
client.team.delete({
|
||||||
where: {
|
where: {
|
||||||
id: teamId,
|
id: teamId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]).then(async result => {
|
||||||
|
await invalidateRedis();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Prisma } from '@/generated/prisma/client';
|
|||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
import { getRandomChars } from '@/lib/generate';
|
import { getRandomChars } from '@/lib/generate';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
|
import redis from '@/lib/redis';
|
||||||
import { sanitizeSortFilters } from '@/lib/sort';
|
import { sanitizeSortFilters } from '@/lib/sort';
|
||||||
import type { QueryFilters, Role } from '@/lib/types';
|
import type { QueryFilters, Role } from '@/lib/types';
|
||||||
|
|
||||||
@@ -129,6 +130,38 @@ export async function deleteUser(userId: string) {
|
|||||||
|
|
||||||
const teamIds = teams.map(a => a.id);
|
const teamIds = teams.map(a => a.id);
|
||||||
|
|
||||||
|
// Cloud mode keeps owned teams (and their team-owned content), so cleanup
|
||||||
|
// only covers user-direct rows. Non-cloud hard-deletes owned teams below,
|
||||||
|
// so we must also clean up team-owned content.
|
||||||
|
const ownedFilter = cloudMode
|
||||||
|
? { userId }
|
||||||
|
: { OR: [{ userId }, { teamId: { in: teamIds } }] };
|
||||||
|
|
||||||
|
const [links, pixels, boards] = await Promise.all([
|
||||||
|
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)];
|
||||||
|
// 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)) {
|
||||||
|
await Promise.all([
|
||||||
|
...linkSlugs.map(slug => redis.client.del(`link:${slug}`)),
|
||||||
|
...pixelSlugs.map(slug => redis.client.del(`pixel:${slug}`)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (cloudMode) {
|
if (cloudMode) {
|
||||||
return transaction([
|
return transaction([
|
||||||
client.website.updateMany({
|
client.website.updateMany({
|
||||||
@@ -146,7 +179,21 @@ export async function deleteUser(userId: string) {
|
|||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
client.share.deleteMany({ where: { entityId: { in: entityIds } } }),
|
||||||
|
// 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();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return transaction([
|
return transaction([
|
||||||
@@ -197,6 +244,10 @@ export async function deleteUser(userId: string) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
client.share.deleteMany({ where: { entityId: { in: entityIds } } }),
|
||||||
|
client.link.deleteMany({ where: ownedFilter }),
|
||||||
|
client.pixel.deleteMany({ where: ownedFilter }),
|
||||||
|
client.board.deleteMany({ where: ownedFilter }),
|
||||||
client.website.deleteMany({
|
client.website.deleteMany({
|
||||||
where: { id: { in: websiteIds } },
|
where: { id: { in: websiteIds } },
|
||||||
}),
|
}),
|
||||||
@@ -205,5 +256,8 @@ export async function deleteUser(userId: string) {
|
|||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]).then(async result => {
|
||||||
|
await invalidateRedis();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user