diff --git a/server/openapi.json b/server/openapi.json index 058e934b7..f62062332 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -4657,6 +4657,11 @@ "repeat": { "type": "number", "minimum": 0 + }, + "expiry": { + "type": "string", + "nullable": true, + "format": "date-time" } }, "required": ["monitors", "name", "duration", "durationUnit", "start", "end", "repeat"] @@ -5185,6 +5190,11 @@ "nullable": true, "format": "date-time" }, + "expiry": { + "type": "string", + "nullable": true, + "format": "date-time" + }, "monitors": { "type": "array", "items": { diff --git a/server/src/db/migration/0007_dropMaintenanceWindowExpiryIndex.ts b/server/src/db/migration/0007_dropMaintenanceWindowExpiryIndex.ts deleted file mode 100644 index 38e55c0d7..000000000 --- a/server/src/db/migration/0007_dropMaintenanceWindowExpiryIndex.ts +++ /dev/null @@ -1,58 +0,0 @@ -import mongoose from "mongoose"; -import { logger } from "@/utils/logger.js"; - -/** - * Drops the TTL index on MaintenanceWindow.expiry and unsets the field on existing rows. - * - * Why: a previous version copied `end` into `expiry` for one-time windows. Combined with the - * TTL index on `expiry`, this caused Mongo to silently delete rows whose `end` was in the past - * at creation time (a common symptom for automation tools sending non-UTC timestamps). The - * fix stops writing `expiry`; this migration removes the live exposure on existing installs. - */ -export async function dropMaintenanceWindowExpiryIndex(): Promise { - const SERVICE_NAME = "Migration:DropMaintenanceWindowExpiryIndex"; - - try { - logger.info({ - service: SERVICE_NAME, - message: "Starting drop of MaintenanceWindow.expiry TTL index", - }); - - const db = mongoose.connection.db; - if (!db) { - throw new Error("Mongo connection not initialized"); - } - const collection = db.collection("maintenancewindows"); - - const indexes = await collection.indexes(); - const expiryIndex = indexes.find( - (idx) => idx.expireAfterSeconds !== undefined && idx.key && Object.keys(idx.key).length === 1 && idx.key.expiry === 1 - ); - - if (expiryIndex?.name) { - await collection.dropIndex(expiryIndex.name); - logger.info({ - service: SERVICE_NAME, - message: `Dropped TTL index ${expiryIndex.name} on MaintenanceWindow.expiry`, - }); - } else { - logger.info({ - service: SERVICE_NAME, - message: "No TTL index found on MaintenanceWindow.expiry", - }); - } - - const result = await collection.updateMany({ expiry: { $exists: true } }, { $unset: { expiry: "" } }); - logger.info({ - service: SERVICE_NAME, - message: `Unset expiry on ${result.modifiedCount} maintenance windows`, - }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - logger.error({ - service: SERVICE_NAME, - message: `Error dropping expiry index: ${errorMessage}`, - }); - throw error; - } -} diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts index fcf0cf1fb..5b74a96fc 100644 --- a/server/src/db/migration/index.ts +++ b/server/src/db/migration/index.ts @@ -5,7 +5,6 @@ import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds. import MigrationModel from "../models/Migration.js"; import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js"; import { cleanupDuplicateMonitorStatsForUniqueIndex } from "./0006_cleanupDuplicateMonitorStatsForUniqueIndex.js"; -import { dropMaintenanceWindowExpiryIndex } from "./0007_dropMaintenanceWindowExpiryIndex.js"; import type { ILogger } from "@/utils/logger.js"; type MigrationEntry = { @@ -20,7 +19,6 @@ const migrations: MigrationEntry[] = [ { name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds }, { name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray }, { name: "0006_cleanupDuplicateMonitorStatsForUniqueIndex", execute: cleanupDuplicateMonitorStatsForUniqueIndex }, - { name: "0007_dropMaintenanceWindowExpiryIndex", execute: dropMaintenanceWindowExpiryIndex }, ]; const runMigrations = async (logger?: ILogger) => { diff --git a/server/src/db/models/MaintenanceWindow.ts b/server/src/db/models/MaintenanceWindow.ts index 1fc9b5f4a..703c144c8 100644 --- a/server/src/db/models/MaintenanceWindow.ts +++ b/server/src/db/models/MaintenanceWindow.ts @@ -6,6 +6,7 @@ type MaintenanceWindowDocumentBase = Omit( end: { type: Date, }, + expiry: { + type: Date, + index: { expires: "0s" }, + }, }, { timestamps: true, diff --git a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts index 0cdda805e..ad96328c5 100644 --- a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts +++ b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts @@ -45,6 +45,11 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository create = async (data: Partial): Promise => { const maintenanceWindow = new MaintenanceWindowModel(data); + + // If the maintenance window is a one time window, set the expiry to the end date + if (maintenanceWindow.repeat === 0) { + maintenanceWindow.expiry = maintenanceWindow.end; + } const result = await maintenanceWindow.save(); return this.toEntity(result); }; diff --git a/server/src/validation/maintenanceWindowValidation.ts b/server/src/validation/maintenanceWindowValidation.ts index bdd11df5f..c53e39b20 100644 --- a/server/src/validation/maintenanceWindowValidation.ts +++ b/server/src/validation/maintenanceWindowValidation.ts @@ -18,6 +18,7 @@ export const createMaintenanceWindowBodyValidation = z start: dateToString, end: dateToString, repeat: z.number().min(0, "Repeat must be a non-negative number"), + expiry: dateToString.optional(), }) .strict() .superRefine((data, ctx) => { @@ -70,6 +71,7 @@ export const editMaintenanceByIdWindowBodyValidation = z repeat: z.number().optional(), start: dateToString.optional(), end: dateToString.optional(), + expiry: dateToString.optional(), monitors: z.array(z.string()).optional(), duration: z.number().optional(), durationUnit: z.enum(DurationUnits).optional(),