Openstatus www.openstatus.dev

fix: reset status report affected monitors on reorder (#1781)

* fix: reset status report affected monitors on reorder

* fix: group error

authored by

Maximilian Kaske and committed by
GitHub
5d972bc6 33814bd1

+159 -34
+94 -33
packages/api/src/router/page.ts
··· 14 14 syncMonitorGroupDeleteMany, 15 15 syncMonitorGroupInsert, 16 16 syncMonitorsToPageDelete, 17 - syncMonitorsToPageDeleteByPage, 18 17 syncMonitorsToPageInsertMany, 18 + syncMonitorsToPageUpsertMany, 19 19 } from "@openstatus/db"; 20 20 import { 21 21 incidentTable, ··· 967 967 } 968 968 969 969 await opts.ctx.db.transaction(async (tx) => { 970 - // Get existing monitor groups to delete from page components 970 + // Get existing state 971 + const existingMonitorsToPages = await tx 972 + .select() 973 + .from(monitorsToPages) 974 + .where(eq(monitorsToPages.pageId, opts.input.id)) 975 + .all(); 976 + 971 977 const existingGroups = await tx.query.monitorGroup.findMany({ 972 978 where: eq(monitorGroup.pageId, opts.input.id), 973 979 }); 974 980 const existingGroupIds = existingGroups.map((g) => g.id); 975 981 976 - // Delete child records first to avoid foreign key constraint violation 977 - await tx 978 - .delete(monitorsToPages) 979 - .where(eq(monitorsToPages.pageId, opts.input.id)); 980 - await tx 981 - .delete(monitorGroup) 982 - .where(eq(monitorGroup.pageId, opts.input.id)); 982 + // Calculate what monitors are in the new input vs existing 983 + const existingMonitorIds = existingMonitorsToPages.map( 984 + (m) => m.monitorId, 985 + ); 986 + 987 + // Find monitors that are being removed (in DB but not in input) 988 + const removedMonitorIds = existingMonitorIds.filter( 989 + (id) => !allMonitorIds.includes(id), 990 + ); 991 + 992 + // Delete removed monitors from monitorsToPages and page components 993 + if (removedMonitorIds.length > 0) { 994 + await tx 995 + .delete(monitorsToPages) 996 + .where( 997 + and( 998 + eq(monitorsToPages.pageId, opts.input.id), 999 + inArray(monitorsToPages.monitorId, removedMonitorIds), 1000 + ), 1001 + ); 1002 + 1003 + // Sync delete to page components 1004 + for (const monitorId of removedMonitorIds) { 1005 + await syncMonitorsToPageDelete(tx, { 1006 + monitorId, 1007 + pageId: opts.input.id, 1008 + }); 1009 + } 1010 + } 1011 + 1012 + // Clear monitorGroupId from all monitorsToPages before deleting groups 1013 + // This prevents foreign key constraint errors 1014 + if (existingGroupIds.length > 0) { 1015 + await tx 1016 + .update(monitorsToPages) 1017 + .set({ monitorGroupId: null }) 1018 + .where( 1019 + and( 1020 + eq(monitorsToPages.pageId, opts.input.id), 1021 + inArray(monitorsToPages.monitorGroupId, existingGroupIds), 1022 + ), 1023 + ); 1024 + } 983 1025 984 - // Sync deletes to page components 985 - await syncMonitorsToPageDeleteByPage(tx, opts.input.id); 1026 + // Handle groups: delete old groups and create new ones 986 1027 if (existingGroupIds.length > 0) { 1028 + await tx 1029 + .delete(monitorGroup) 1030 + .where(eq(monitorGroup.pageId, opts.input.id)); 1031 + 1032 + // Sync delete page component groups 987 1033 await syncMonitorGroupDeleteMany(tx, existingGroupIds); 988 1034 } 989 1035 1036 + // Create new monitor groups 1037 + let monitorGroups: Array<{ id: number; name: string }> = []; 990 1038 if (opts.input.groups.length > 0) { 991 - const monitorGroups = await tx 1039 + monitorGroups = await tx 992 1040 .insert(monitorGroup) 993 1041 .values( 994 1042 opts.input.groups.map((g) => ({ ··· 1008 1056 name: group.name, 1009 1057 }); 1010 1058 } 1011 - 1012 - const groupMonitorValues = opts.input.groups.flatMap((g, i) => 1013 - g.monitors.map((m) => ({ 1014 - pageId: opts.input.id, 1015 - monitorId: m.id, 1016 - order: g.order, 1017 - monitorGroupId: monitorGroups[i].id, 1018 - groupOrder: m.order, 1019 - })), 1020 - ); 1021 - 1022 - await tx.insert(monitorsToPages).values(groupMonitorValues); 1023 - // Sync to page components 1024 - await syncMonitorsToPageInsertMany(tx, groupMonitorValues); 1025 1059 } 1026 1060 1027 - if (opts.input.monitors.length > 0) { 1028 - const monitorValues = opts.input.monitors.map((m) => ({ 1061 + // Prepare values for upsert - both grouped and ungrouped monitors 1062 + const groupMonitorValues = opts.input.groups.flatMap((g, i) => 1063 + g.monitors.map((m) => ({ 1029 1064 pageId: opts.input.id, 1030 1065 monitorId: m.id, 1031 - order: m.order, 1032 - })); 1066 + order: g.order, 1067 + monitorGroupId: monitorGroups[i].id, 1068 + groupOrder: m.order, 1069 + })), 1070 + ); 1033 1071 1034 - await tx.insert(monitorsToPages).values(monitorValues); 1035 - // Sync to page components 1036 - await syncMonitorsToPageInsertMany(tx, monitorValues); 1072 + const monitorValues = opts.input.monitors.map((m) => ({ 1073 + pageId: opts.input.id, 1074 + monitorId: m.id, 1075 + order: m.order, 1076 + monitorGroupId: null as number | null, 1077 + groupOrder: 0, 1078 + })); 1079 + 1080 + const allValues = [...groupMonitorValues, ...monitorValues]; 1081 + 1082 + // Upsert all monitors (update existing, insert new) 1083 + if (allValues.length > 0) { 1084 + await tx 1085 + .insert(monitorsToPages) 1086 + .values(allValues) 1087 + .onConflictDoUpdate({ 1088 + target: [monitorsToPages.monitorId, monitorsToPages.pageId], 1089 + set: { 1090 + order: sql.raw("excluded.`order`"), 1091 + monitorGroupId: sql.raw("excluded.`monitor_group_id`"), 1092 + groupOrder: sql.raw("excluded.`group_order`"), 1093 + }, 1094 + }); 1095 + 1096 + // Sync upsert to page components (updates existing, inserts new) 1097 + await syncMonitorsToPageUpsertMany(tx, allValues); 1037 1098 } 1038 1099 }); 1039 1100 }),
+65 -1
packages/db/src/sync.ts
··· 1 - import { and, eq, inArray } from "drizzle-orm"; 1 + import { and, eq, inArray, sql } from "drizzle-orm"; 2 2 3 3 import type { db } from "./db"; 4 4 import { ··· 168 168 if (values.length === 0) return; 169 169 170 170 await db.insert(pageComponent).values(values).onConflictDoNothing(); 171 + } 172 + 173 + /** 174 + * Syncs multiple monitors_to_pages upserts to page_component 175 + * Updates order, groupId, groupOrder for existing components, inserts new ones 176 + */ 177 + export async function syncMonitorsToPageUpsertMany( 178 + db: DB | Transaction, 179 + items: Array<{ 180 + monitorId: number; 181 + pageId: number; 182 + order?: number; 183 + monitorGroupId?: number | null; 184 + groupOrder?: number; 185 + }>, 186 + ) { 187 + if (items.length === 0) return; 188 + 189 + // Get all monitor data in one query 190 + const monitorIds = [...new Set(items.map((item) => item.monitorId))]; 191 + const monitors = await db 192 + .select({ 193 + id: monitor.id, 194 + name: monitor.name, 195 + externalName: monitor.externalName, 196 + workspaceId: monitor.workspaceId, 197 + }) 198 + .from(monitor) 199 + .where(inArray(monitor.id, monitorIds)); 200 + 201 + const monitorMap = new Map(monitors.map((m) => [m.id, m])); 202 + 203 + const values = items 204 + .map((item) => { 205 + const m = monitorMap.get(item.monitorId); 206 + if (!m || !m.workspaceId) return null; 207 + return { 208 + workspaceId: m.workspaceId, 209 + pageId: item.pageId, 210 + type: "monitor" as const, 211 + monitorId: item.monitorId, 212 + name: m.externalName || m.name, 213 + order: item.order ?? 0, 214 + groupId: item.monitorGroupId ?? null, 215 + groupOrder: item.groupOrder ?? 0, 216 + }; 217 + }) 218 + .filter((v): v is NonNullable<typeof v> => v !== null); 219 + 220 + if (values.length === 0) return; 221 + 222 + // Use onConflictDoUpdate to update existing page components 223 + // The unique constraint is on (pageId, monitorId) 224 + await db 225 + .insert(pageComponent) 226 + .values(values) 227 + .onConflictDoUpdate({ 228 + target: [pageComponent.pageId, pageComponent.monitorId], 229 + set: { 230 + order: sql.raw("excluded.`order`"), 231 + groupId: sql.raw("excluded.`group_id`"), 232 + groupOrder: sql.raw("excluded.`group_order`"), 233 + }, 234 + }); 171 235 } 172 236 173 237 /**