refactor(maintenance): tighten validator + migration per /simplify

- Collapse two refines into a single superRefine in createMaintenanceWindowBodyValidation; same on edit.
- Match peer migration convention: use mongoose.connection.db directly, not Model.collection.
- Tighten TTL index detection: require expireAfterSeconds + single-field key.
- Add Starting log at migration top to match peers 0003-0006.
This commit is contained in:
gorkem-bwl committed 2026-05-07 12:33:03 -04:00
1 parent 4db95cfa44
commit bd059da94a
2 files changed
+49 -20

No files matched your search

@@ -1,23 +1,33 @@
import { MaintenanceWindowModel } from "../models/MaintenanceWindow.js";
import mongoose from "mongoose";
import { logger } from "@/utils/logger.js";
/**
* Drops the TTL index on MaintenanceWindow.expiry and unsets the expiry field.
* Drops the TTL index on MaintenanceWindow.expiry and unsets the field on existing rows.
*
* Earlier versions auto-set expiry = end for one-time maintenance windows. Combined with the
* TTL index ({ expires: "0s" }), this caused Mongo to silently delete completed one-time
* windows shortly after their end-date passed — including windows whose end-date was already
* in the past at creation time (e.g. timezone-mismatched payloads from automation tools).
* The fix stops writing expiry on create, and this migration removes the index and stale field
* so legacy rows are no longer at risk of being deleted by the TTL monitor.
* 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 {
const collection = MaintenanceWindowModel.collection;
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.key && idx.key.expiry === 1);
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);
@@ -19,13 +19,23 @@ export const createMaintenanceWindowBodyValidation = z
repeat: z.number().min(0, "Repeat must be a non-negative number"),
})
.strict()
.refine((data) => new Date(data.end).getTime() > new Date(data.start).getTime(), {
message: "End must be after start",
path: ["end"],
})
.refine((data) => data.repeat > 0 || new Date(data.end).getTime() > Date.now(), {
message: "End must be in the future for one-time maintenance windows",
path: ["end"],
.superRefine((data, ctx) => {
const start = new Date(data.start).getTime();
const end = new Date(data.end).getTime();
if (end <= start) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "End must be after start",
path: ["end"],
});
}
if (data.repeat === 0 && end <= Date.now()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "End must be in the future for one-time maintenance windows",
path: ["end"],
});
}
});
export const getMaintenanceWindowByIdParamValidation = z.object({
@@ -64,7 +74,16 @@ export const editMaintenanceByIdWindowBodyValidation = z
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(),
})
.strict()
.refine((data) => !data.start || !data.end || new Date(data.end).getTime() > new Date(data.start).getTime(), {
message: "End must be after start",
path: ["end"],
.superRefine((data, ctx) => {
if (data.start && data.end) {
const start = new Date(data.start).getTime();
const end = new Date(data.end).getTime();
if (end <= start) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "End must be after start",
path: ["end"],
});
}
}
});