Skip to content

Commit 872d737

Browse files
committed
fix: cascade websites on user/team delete + filter soft-deleted in list queries
Stacks on top of #4243. Five adjacent bugs from the same family: - deleteTeam left team-owned websites (and all dependent rows) orphaned. Added inline cleanup mirroring deleteWebsite. - non-cloud deleteUser, when hard-deleting the user's owned teams, also left team-owned websites orphaned. Extended the existing ownedFilter pattern (cloud-gated OR) to cover websites. - getTeamLinks/getUserPixels/getTeamPixels did not filter deletedAt: null, leaking soft-deleted entries into list views. - cloud deleteUser restamped already-soft-deleted websites' deleted_at; added deletedAt: null guard (same shape as link/pixel restamping fix). - Surfaced pre-existing gaps in deleteUser non-cloud: missing sessionReplaySaved/sessionReplay/revenue/segment cleanups, entityIds excluded website ids so website-shares were orphaned.
1 parent 71ee000 commit 872d737

4 files changed

Lines changed: 58 additions & 32 deletions

File tree

src/queries/prisma/link.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export async function getTeamLinks(teamId: string, filters?: QueryFilters) {
4747
{
4848
where: {
4949
teamId,
50+
deletedAt: null,
5051
},
5152
},
5253
filters,

src/queries/prisma/pixel.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export async function getUserPixels(userId: string, filters?: QueryFilters) {
3030
{
3131
where: {
3232
userId,
33+
deletedAt: null,
3334
},
3435
},
3536
filters,
@@ -41,6 +42,7 @@ export async function getTeamPixels(teamId: string, filters?: QueryFilters) {
4142
{
4243
where: {
4344
teamId,
45+
deletedAt: null,
4446
},
4547
},
4648
filters,

src/queries/prisma/team.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export async function deleteTeam(teamId: string) {
145145
const { client, transaction } = prisma;
146146
const cloudMode = !!process.env.CLOUD_MODE;
147147

148-
const [links, pixels, boards] = await Promise.all([
148+
const [links, pixels, boards, websites] = await Promise.all([
149149
client.link.findMany({
150150
where: { teamId },
151151
select: { id: true, slug: true, deletedAt: true },
@@ -155,17 +155,26 @@ export async function deleteTeam(teamId: string) {
155155
select: { id: true, slug: true, deletedAt: true },
156156
}),
157157
client.board.findMany({ where: { teamId }, select: { id: true } }),
158+
client.website.findMany({ where: { teamId }, select: { id: true, deletedAt: true } }),
158159
]);
159-
const entityIds = [...links.map(l => l.id), ...pixels.map(p => p.id), ...boards.map(b => b.id)];
160-
// Only invalidate Redis cache for slugs that are still live (not already soft-deleted).
160+
const websiteIds = websites.map(w => w.id);
161+
const entityIds = [
162+
...links.map(l => l.id),
163+
...pixels.map(p => p.id),
164+
...boards.map(b => b.id),
165+
...websiteIds,
166+
];
167+
// Only invalidate Redis cache for slugs/keys that are still live (not already soft-deleted).
161168
const linkSlugs = links.filter(l => !l.deletedAt).map(l => l.slug);
162169
const pixelSlugs = pixels.filter(p => !p.deletedAt).map(p => p.slug);
170+
const liveWebsiteIds = websites.filter(w => !w.deletedAt).map(w => w.id);
163171

164172
const invalidateRedis = async () => {
165-
if (redis.enabled && (linkSlugs.length || pixelSlugs.length)) {
173+
if (redis.enabled && (linkSlugs.length || pixelSlugs.length || liveWebsiteIds.length)) {
166174
await Promise.all([
167175
...linkSlugs.map(slug => redis.client.del(`link:${slug}`)),
168176
...pixelSlugs.map(slug => redis.client.del(`pixel:${slug}`)),
177+
...liveWebsiteIds.map(id => redis.client.del(`website:${id}`)),
169178
]);
170179
}
171180
};
@@ -191,6 +200,10 @@ export async function deleteTeam(teamId: string) {
191200
where: { teamId, deletedAt: null },
192201
}),
193202
client.board.deleteMany({ where: { teamId } }),
203+
client.website.updateMany({
204+
data: { deletedAt: new Date() },
205+
where: { teamId, deletedAt: null },
206+
}),
194207
]).then(async result => {
195208
await invalidateRedis();
196209
return result;
@@ -207,6 +220,17 @@ export async function deleteTeam(teamId: string) {
207220
client.link.deleteMany({ where: { teamId } }),
208221
client.pixel.deleteMany({ where: { teamId } }),
209222
client.board.deleteMany({ where: { teamId } }),
223+
// Mirror deleteWebsite cleanup order for team-owned websites:
224+
client.sessionReplaySaved.deleteMany({ where: { websiteId: { in: websiteIds } } }),
225+
client.sessionReplay.deleteMany({ where: { websiteId: { in: websiteIds } } }),
226+
client.revenue.deleteMany({ where: { websiteId: { in: websiteIds } } }),
227+
client.eventData.deleteMany({ where: { websiteId: { in: websiteIds } } }),
228+
client.sessionData.deleteMany({ where: { websiteId: { in: websiteIds } } }),
229+
client.websiteEvent.deleteMany({ where: { websiteId: { in: websiteIds } } }),
230+
client.session.deleteMany({ where: { websiteId: { in: websiteIds } } }),
231+
client.report.deleteMany({ where: { websiteId: { in: websiteIds } } }),
232+
client.segment.deleteMany({ where: { websiteId: { in: websiteIds } } }),
233+
client.website.deleteMany({ where: { id: { in: websiteIds } } }),
210234
client.team.delete({
211235
where: {
212236
id: teamId,

src/queries/prisma/user.ts

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,6 @@ export async function deleteUser(userId: string) {
104104
const { client, transaction } = prisma;
105105
const cloudMode = !!process.env.CLOUD_MODE;
106106

107-
const websites = await client.website.findMany({
108-
where: { userId },
109-
});
110-
111-
let websiteIds = [];
112-
113-
if (websites.length > 0) {
114-
websiteIds = websites.map(a => a.id);
115-
}
116-
117107
const teams = await client.team.findMany({
118108
where: {
119109
members: {
@@ -129,12 +119,12 @@ export async function deleteUser(userId: string) {
129119

130120
// Cloud mode keeps owned teams (and their team-owned content), so cleanup
131121
// only covers user-direct rows. Non-cloud hard-deletes owned teams below,
132-
// so we must also clean up team-owned content.
122+
// so we must also clean up team-owned content (websites included).
133123
const ownedFilter = cloudMode
134124
? { userId }
135125
: { OR: [{ userId }, { teamId: { in: teamIds } }] };
136126

137-
const [links, pixels, boards] = await Promise.all([
127+
const [links, pixels, boards, websites] = await Promise.all([
138128
client.link.findMany({
139129
where: ownedFilter,
140130
select: { id: true, slug: true, deletedAt: true },
@@ -144,17 +134,29 @@ export async function deleteUser(userId: string) {
144134
select: { id: true, slug: true, deletedAt: true },
145135
}),
146136
client.board.findMany({ where: ownedFilter, select: { id: true } }),
137+
client.website.findMany({
138+
where: ownedFilter,
139+
select: { id: true, deletedAt: true },
140+
}),
147141
]);
148-
const entityIds = [...links.map(l => l.id), ...pixels.map(p => p.id), ...boards.map(b => b.id)];
149-
// Only invalidate Redis cache for slugs that are still live (not already soft-deleted).
142+
const websiteIds = websites.map(w => w.id);
143+
const entityIds = [
144+
...links.map(l => l.id),
145+
...pixels.map(p => p.id),
146+
...boards.map(b => b.id),
147+
...websiteIds,
148+
];
149+
// Only invalidate Redis cache for slugs/keys that are still live (not already soft-deleted).
150150
const linkSlugs = links.filter(l => !l.deletedAt).map(l => l.slug);
151151
const pixelSlugs = pixels.filter(p => !p.deletedAt).map(p => p.slug);
152+
const liveWebsiteIds = websites.filter(w => !w.deletedAt).map(w => w.id);
152153

153154
const invalidateRedis = async () => {
154-
if (redis.enabled && (linkSlugs.length || pixelSlugs.length)) {
155+
if (redis.enabled && (linkSlugs.length || pixelSlugs.length || liveWebsiteIds.length)) {
155156
await Promise.all([
156157
...linkSlugs.map(slug => redis.client.del(`link:${slug}`)),
157158
...pixelSlugs.map(slug => redis.client.del(`pixel:${slug}`)),
159+
...liveWebsiteIds.map(id => redis.client.del(`website:${id}`)),
158160
]);
159161
}
160162
};
@@ -165,7 +167,7 @@ export async function deleteUser(userId: string) {
165167
data: {
166168
deletedAt: new Date(),
167169
},
168-
where: { id: { in: websiteIds } },
170+
where: { id: { in: websiteIds }, deletedAt: null },
169171
}),
170172
client.user.update({
171173
data: {
@@ -194,18 +196,15 @@ export async function deleteUser(userId: string) {
194196
}
195197

196198
return transaction([
197-
client.eventData.deleteMany({
198-
where: { websiteId: { in: websiteIds } },
199-
}),
200-
client.sessionData.deleteMany({
201-
where: { websiteId: { in: websiteIds } },
202-
}),
203-
client.websiteEvent.deleteMany({
204-
where: { websiteId: { in: websiteIds } },
205-
}),
206-
client.session.deleteMany({
207-
where: { websiteId: { in: websiteIds } },
208-
}),
199+
// Website-dependent rows (mirror deleteWebsite cleanup at queries/prisma/website.ts):
200+
client.sessionReplaySaved.deleteMany({ where: { websiteId: { in: websiteIds } } }),
201+
client.sessionReplay.deleteMany({ where: { websiteId: { in: websiteIds } } }),
202+
client.revenue.deleteMany({ where: { websiteId: { in: websiteIds } } }),
203+
client.eventData.deleteMany({ where: { websiteId: { in: websiteIds } } }),
204+
client.sessionData.deleteMany({ where: { websiteId: { in: websiteIds } } }),
205+
client.websiteEvent.deleteMany({ where: { websiteId: { in: websiteIds } } }),
206+
client.session.deleteMany({ where: { websiteId: { in: websiteIds } } }),
207+
client.segment.deleteMany({ where: { websiteId: { in: websiteIds } } }),
209208
client.teamUser.deleteMany({
210209
where: {
211210
OR: [

0 commit comments

Comments
 (0)