fix(maintenance): stop silent deletion of one-time maintenance windows
Maintenance windows could disappear seconds after creation when the API received a payload with end dates already in the past. The repository auto-set expiry = end for one-time windows (repeat = 0), and Mongo's TTL index on the expiry field then deleted the row on its next sweep. This was particularly visible with automation tools sending payloads in non-UTC timezones. ## Changes ### Validator hardening - Add .strict() to create + edit body schemas so unknown keys return 400 instead of being silently dropped. - Refine end > start on create. - Refine end > now for one-time create payloads (repeat = 0). - Refine end > start on edit when both are provided. - Remove the accepted expiry field from both schemas. ### Stop the auto-delete behaviour - Repository.create no longer copies end into expiry. - Schema's expiry field + TTL index removed. - Migration 0007 drops the live TTL index and unsets stale expiry values on existing rows so legacy data is no longer at risk. ### Client - Form submission no longer includes expiry.
This commit is contained in:
1 parent
a69f8a5ab5
commit
4db95cfa44
6 files changed
+86
-36
No files matched your search
@@ -7,17 +7,26 @@ const dateToString = z.coerce.date().transform((d) => d.toISOString());
|
||||
// Maintenance Window Validations
|
||||
//****************************************
|
||||
|
||||
export const createMaintenanceWindowBodyValidation = z.object({
|
||||
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
active: z.boolean().optional(),
|
||||
duration: z.number().min(1, "Duration is required"),
|
||||
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]),
|
||||
start: dateToString,
|
||||
end: dateToString,
|
||||
repeat: z.number().min(0, "Repeat must be a non-negative number"),
|
||||
expiry: dateToString.optional(),
|
||||
});
|
||||
export const createMaintenanceWindowBodyValidation = z
|
||||
.object({
|
||||
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
active: z.boolean().optional(),
|
||||
duration: z.number().min(1, "Duration is required"),
|
||||
durationUnit: z.enum(["seconds", "minutes", "hours", "days"]),
|
||||
start: dateToString,
|
||||
end: dateToString,
|
||||
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"],
|
||||
});
|
||||
|
||||
export const getMaintenanceWindowByIdParamValidation = z.object({
|
||||
id: z.string().min(1, "ID is required"),
|
||||
@@ -43,14 +52,19 @@ export const editMaintenanceWindowByIdParamValidation = z.object({
|
||||
id: z.string().min(1, "ID is required"),
|
||||
});
|
||||
|
||||
export const editMaintenanceByIdWindowBodyValidation = z.object({
|
||||
active: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
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(["seconds", "minutes", "hours", "days"]).optional(),
|
||||
});
|
||||
export const editMaintenanceByIdWindowBodyValidation = z
|
||||
.object({
|
||||
active: z.boolean().optional(),
|
||||
name: z.string().optional(),
|
||||
repeat: z.number().optional(),
|
||||
start: dateToString.optional(),
|
||||
end: dateToString.optional(),
|
||||
monitors: z.array(z.string()).optional(),
|
||||
duration: z.number().optional(),
|
||||
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"],
|
||||
});
|
||||
Reference in new issue
Block a user