From 0944dba386f3a05d7075c2aba2f5efc935d31f30 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 25 Mar 2026 13:51:23 -0700 Subject: [PATCH] add maintenance-windows repo --- .../TimescaleMaintenanceWindowsRepository.ts | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 server/src/repositories/maintenance-windows/TimescaleMaintenanceWindowsRepository.ts diff --git a/server/src/repositories/maintenance-windows/TimescaleMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/TimescaleMaintenanceWindowsRepository.ts new file mode 100644 index 000000000..cfe28198a --- /dev/null +++ b/server/src/repositories/maintenance-windows/TimescaleMaintenanceWindowsRepository.ts @@ -0,0 +1,197 @@ +import type { Pool } from "pg"; +import type { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js"; +import type { MaintenanceWindow, DurationUnit } from "@/types/maintenanceWindow.js"; +import { AppError } from "@/utils/AppError.js"; + +interface MaintenanceWindowRow { + id: string; + monitor_id: string; + team_id: string; + active: boolean; + name: string; + duration: number; + duration_unit: DurationUnit; + repeat: number; + start_time: Date; + end_time: Date; + expiry: Date | null; + created_at: Date; + updated_at: Date; +} + +const COLUMNS = `id, monitor_id, team_id, active, name, duration, duration_unit, repeat, + start_time, end_time, expiry, created_at, updated_at`; + +export class TimescaleMaintenanceWindowsRepository implements IMaintenanceWindowsRepository { + constructor(private pool: Pool) {} + + create = async (data: Partial): Promise => { + const startTime = data.start ? new Date(data.start) : null; + const endTime = data.end ? new Date(data.end) : null; + // One-time windows expire at end time + const expiry = data.repeat === 0 ? endTime : null; + + const result = await this.pool.query( + `INSERT INTO maintenance_windows (monitor_id, team_id, active, name, duration, duration_unit, repeat, start_time, end_time, expiry) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + RETURNING ${COLUMNS}`, + [ + data.monitorId, + data.teamId, + data.active ?? true, + data.name ?? null, + data.duration ?? null, + data.durationUnit ?? null, + data.repeat ?? 0, + startTime, + endTime, + expiry, + ] + ); + const row = result.rows[0]; + if (!row) { + throw new AppError({ message: "Failed to create maintenance window", status: 500 }); + } + return this.toEntity(row); + }; + + findById = async (id: string, teamId: string): Promise => { + const result = await this.pool.query(`SELECT ${COLUMNS} FROM maintenance_windows WHERE id = $1 AND team_id = $2`, [ + id, + teamId, + ]); + const row = result.rows[0]; + if (!row) { + throw new AppError({ message: "Maintenance Window not found", status: 404 }); + } + return this.toEntity(row); + }; + + findByMonitorId = async (monitorId: string, teamId: string): Promise => { + const result = await this.pool.query(`SELECT ${COLUMNS} FROM maintenance_windows WHERE monitor_id = $1 AND team_id = $2`, [ + monitorId, + teamId, + ]); + return result.rows.map(this.toEntity); + }; + + findByTeamId = async ( + teamId: string, + page: number, + rowsPerPage: number, + field?: string, + order?: string, + active?: boolean + ): Promise => { + const conditions: string[] = ["team_id = $1"]; + const values: unknown[] = [teamId]; + let paramIndex = 2; + + if (active !== undefined) { + conditions.push(`active = $${paramIndex++}`); + values.push(active); + } + + const fieldMap: Record = { + createdAt: "created_at", + name: "name", + start: "start_time", + end: "end_time", + active: "active", + }; + const sortColumn = field ? (fieldMap[field] ?? "created_at") : "created_at"; + const sortDirection = order === "asc" ? "ASC" : "DESC"; + + const offset = page && rowsPerPage ? page * rowsPerPage : 0; + + const result = await this.pool.query( + `SELECT ${COLUMNS} FROM maintenance_windows + WHERE ${conditions.join(" AND ")} + ORDER BY ${sortColumn} ${sortDirection} + LIMIT $${paramIndex++} OFFSET $${paramIndex}`, + [...values, rowsPerPage, offset] + ); + return result.rows.map(this.toEntity); + }; + + updateById = async (id: string, teamId: string, patch: Partial): Promise => { + const sets: string[] = []; + const values: unknown[] = []; + let paramIndex = 1; + + const fieldMap: [keyof MaintenanceWindow, string, boolean][] = [ + ["active", "active", false], + ["name", "name", false], + ["duration", "duration", false], + ["durationUnit", "duration_unit", false], + ["repeat", "repeat", false], + ["start", "start_time", true], + ["end", "end_time", true], + ]; + + for (const [key, column, isDate] of fieldMap) { + if (patch[key] !== undefined) { + sets.push(`${column} = $${paramIndex++}`); + values.push(isDate ? new Date(patch[key] as string) : patch[key]); + } + } + + if (sets.length === 0) { + return this.findById(id, teamId); + } + + sets.push(`updated_at = NOW()`); + values.push(id, teamId); + + const result = await this.pool.query( + `UPDATE maintenance_windows SET ${sets.join(", ")} WHERE id = $${paramIndex++} AND team_id = $${paramIndex} + RETURNING ${COLUMNS}`, + values + ); + const row = result.rows[0]; + if (!row) { + throw new AppError({ message: "Maintenance window not found or could not be updated", status: 404 }); + } + return this.toEntity(row); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const result = await this.pool.query( + `DELETE FROM maintenance_windows WHERE id = $1 AND team_id = $2 RETURNING ${COLUMNS}`, + [id, teamId] + ); + const row = result.rows[0]; + if (!row) { + throw new AppError({ message: "Maintenance window not found or could not be deleted", status: 404 }); + } + return this.toEntity(row); + }; + + countByTeamId = async (teamId: string, active?: boolean): Promise => { + const conditions: string[] = ["team_id = $1"]; + const values: unknown[] = [teamId]; + + if (active !== undefined) { + conditions.push(`active = $2`); + values.push(active); + } + + const result = await this.pool.query(`SELECT COUNT(*)::int AS count FROM maintenance_windows WHERE ${conditions.join(" AND ")}`, values); + return result.rows[0].count; + }; + + private toEntity = (row: MaintenanceWindowRow): MaintenanceWindow => ({ + id: row.id, + monitorId: row.monitor_id, + teamId: row.team_id, + active: row.active, + name: row.name, + duration: row.duration, + durationUnit: row.duration_unit, + repeat: row.repeat, + start: row.start_time.toISOString(), + end: row.end_time.toISOString(), + createdAt: row.created_at.toISOString(), + updatedAt: row.updated_at.toISOString(), + }); +}