diff --git a/apps/server/src/routes/rpc/services/monitor/__tests__/monitor.test.ts b/apps/server/src/routes/rpc/services/monitor/__tests__/monitor.test.ts index e6318feacf..0cb860c67d 100644 --- a/apps/server/src/routes/rpc/services/monitor/__tests__/monitor.test.ts +++ b/apps/server/src/routes/rpc/services/monitor/__tests__/monitor.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { db, eq } from "@openstatus/db"; -import { monitor } from "@openstatus/db/src/schema"; +import { monitor, pageComponent } from "@openstatus/db/src/schema"; import { monitorStatusTable } from "@openstatus/db/src/schema/monitor_status/monitor_status"; import { app } from "@/index"; @@ -468,6 +468,61 @@ describe("MonitorService.DeleteMonitor", () => { await db.delete(monitor).where(eq(monitor.id, otherWorkspaceMon.id)); } }); + + test("deleting a monitor removes its page components", async () => { + // Create a monitor + const mon = await db + .insert(monitor) + .values({ + workspaceId: 1, + name: `${TEST_PREFIX}-delete-with-component`, + url: "https://delete-component.example.com", + periodicity: "1m", + active: true, + regions: "ams", + jobType: "http", + }) + .returning() + .get(); + + // Create a pageComponent referencing that monitor + const component = await db + .insert(pageComponent) + .values({ + workspaceId: 1, + pageId: 1, + type: "monitor", + monitorId: mon.id, + name: `${TEST_PREFIX}-component`, + order: 0, + }) + .returning() + .get(); + + // Verify the component exists + const beforeDelete = await db + .select() + .from(pageComponent) + .where(eq(pageComponent.id, component.id)) + .get(); + expect(beforeDelete).toBeDefined(); + + // Delete the monitor + const res = await connectRequest( + "DeleteMonitor", + { id: String(mon.id) }, + { "x-openstatus-key": "1" }, + ); + expect(res.status).toBe(200); + + // Verify the page component was removed + const afterDelete = await db + .select() + .from(pageComponent) + .where(eq(pageComponent.id, component.id)) + .get(); + expect(afterDelete).toBeUndefined(); + }); }); describe("MonitorService.CreateHTTPMonitor", () => { diff --git a/apps/server/src/routes/rpc/services/monitor/index.ts b/apps/server/src/routes/rpc/services/monitor/index.ts index 8d45f341bb..127be27d6a 100644 --- a/apps/server/src/routes/rpc/services/monitor/index.ts +++ b/apps/server/src/routes/rpc/services/monitor/index.ts @@ -3,7 +3,16 @@ import { getCheckerPayload, getCheckerUrl } from "@/libs/checker"; import { tb } from "@/libs/clients"; import type { ServiceImpl } from "@connectrpc/connect"; import { and, db, eq, gte, inArray, isNull, sql } from "@openstatus/db"; -import { monitor, monitorRun } from "@openstatus/db/src/schema"; +import { + maintenancesToMonitors, + monitor, + monitorRun, + monitorTagsToMonitors, + monitorsToPages, + monitorsToStatusReport, + notificationsToMonitors, + pageComponent, +} from "@openstatus/db/src/schema"; import { monitorStatusTable } from "@openstatus/db/src/schema/monitor_status/monitor_status"; import { selectMonitorSchema } from "@openstatus/db/src/schema/monitors/validation"; import type { @@ -563,14 +572,34 @@ export const monitorServiceImpl: ServiceImpl = { throw monitorNotFoundError(req.id); } - // Soft delete - await db - .update(monitor) - .set({ - active: false, - deletedAt: new Date(), - }) - .where(eq(monitor.id, dbMon.id)); + // Soft delete and clean up related rows atomically + await db.transaction(async (tx) => { + await tx + .update(monitor) + .set({ + active: false, + deletedAt: new Date(), + }) + .where(eq(monitor.id, dbMon.id)); + await tx + .delete(monitorsToPages) + .where(eq(monitorsToPages.monitorId, dbMon.id)); + await tx + .delete(monitorTagsToMonitors) + .where(eq(monitorTagsToMonitors.monitorId, dbMon.id)); + await tx + .delete(monitorsToStatusReport) + .where(eq(monitorsToStatusReport.monitorId, dbMon.id)); + await tx + .delete(notificationsToMonitors) + .where(eq(notificationsToMonitors.monitorId, dbMon.id)); + await tx + .delete(maintenancesToMonitors) + .where(eq(maintenancesToMonitors.monitorId, dbMon.id)); + await tx + .delete(pageComponent) + .where(eq(pageComponent.monitorId, dbMon.id)); + }); return { success: true }; }, diff --git a/packages/api/src/router/monitor.ts b/packages/api/src/router/monitor.ts index c93541c3cc..0199776721 100644 --- a/packages/api/src/router/monitor.ts +++ b/packages/api/src/router/monitor.ts @@ -27,6 +27,7 @@ import { monitorsToStatusReport, notification, notificationsToMonitors, + pageComponent, privateLocation, privateLocationToMonitors, selectIncidentSchema, @@ -63,13 +64,11 @@ export const monitorRouter = createTRPCRouter({ .get(); if (!monitorToDelete) return; - await opts.ctx.db - .update(monitor) - .set({ deletedAt: new Date(), active: false }) - .where(eq(monitor.id, monitorToDelete.id)) - .run(); - await opts.ctx.db.transaction(async (tx) => { + await tx + .update(monitor) + .set({ deletedAt: new Date(), active: false }) + .where(eq(monitor.id, monitorToDelete.id)); await tx .delete(monitorsToPages) .where(eq(monitorsToPages.monitorId, monitorToDelete.id)); @@ -85,6 +84,9 @@ export const monitorRouter = createTRPCRouter({ await tx .delete(maintenancesToMonitors) .where(eq(maintenancesToMonitors.monitorId, monitorToDelete.id)); + await tx + .delete(pageComponent) + .where(eq(pageComponent.monitorId, monitorToDelete.id)); }); }), @@ -109,13 +111,11 @@ export const monitorRouter = createTRPCRouter({ }); } - await opts.ctx.db - .update(monitor) - .set({ deletedAt: new Date(), active: false }) - .where(inArray(monitor.id, opts.input.ids)) - .run(); - await opts.ctx.db.transaction(async (tx) => { + await tx + .update(monitor) + .set({ deletedAt: new Date(), active: false }) + .where(inArray(monitor.id, opts.input.ids)); await tx .delete(monitorsToPages) .where(inArray(monitorsToPages.monitorId, opts.input.ids)); @@ -131,6 +131,9 @@ export const monitorRouter = createTRPCRouter({ await tx .delete(maintenancesToMonitors) .where(inArray(maintenancesToMonitors.monitorId, opts.input.ids)); + await tx + .delete(pageComponent) + .where(inArray(pageComponent.monitorId, opts.input.ids)); }); }),