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:
1 parent
b462ba7562
commit
c45bcfaa2f
6 files changed
+22
-60
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user