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:
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"],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user