revert(maintenance): keep one-time TTL cleanup, narrow PR to validator-only

Per review feedback: auto-deleting completed one-time maintenance windows is
intentional cleanup, not a bug. The actual bug is past-dated end values being
accepted at creation time, which the validator refines now reject up front.

Reverts:
- Restore expiry field + TTL index on MaintenanceWindow schema
- Restore repository's auto-set of expiry = end for repeat=0
- Drop migration 0007 (no longer needed)
- Restore expiry properties in openapi.json
- Restore expiry in create/edit validators (still optional)

Kept from this PR:
- .strict() on create/edit body schemas
- end > start refine on create/edit
- end > now refine on create when repeat=0 (the real fix)
- DurationUnits const tuple + reuse (rules 5/8/10)
- Client no longer sends redundant expiry (server still derives it)
This commit is contained in:
gorkem-bwl committed 2026-05-07 13:38:10 -04:00
1 parent b462ba7562
commit c45bcfaa2f
6 files changed
+22 -60

No files matched your search

+10
View File
@@ -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": {
@@ -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<void> {
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;
}
}
-2
View File
@@ -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) => {
@@ -6,6 +6,7 @@ type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId"
teamId: Types.ObjectId;
start: Date;
end: Date;
expiry?: Date;
createdAt: Date;
updatedAt: Date;
};
@@ -49,6 +50,10 @@ const MaintenanceWindowSchema = new Schema<MaintenanceWindowDocument>(
end: {
type: Date,
},
expiry: {
type: Date,
index: { expires: "0s" },
},
},
{
timestamps: true,
@@ -45,6 +45,11 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
create = async (data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow> => {
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);
};
@@ -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(),