diff --git a/server/src/db/migration/0005_migrateStatusPageTypeToArray.ts b/server/src/db/migration/0005_migrateStatusPageTypeToArray.ts new file mode 100644 index 000000000..0b13d1cb6 --- /dev/null +++ b/server/src/db/migration/0005_migrateStatusPageTypeToArray.ts @@ -0,0 +1,45 @@ +import mongoose from "mongoose"; +import { logger } from "@/utils/logger.js"; +import StatusPageModel from "../models/StatusPage.js"; + +export async function migrateStatusPageTypeToArray(): Promise { + const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray"; + + try { + logger.info({ service: SERVICE_NAME, message: "Starting migration of StatusPage type field from string to array" }); + + const db = mongoose.connection.db; + if (!db) { + throw new Error("Database connection is not initialized"); + } + + const result = await StatusPageModel.updateMany( + { + $expr: { + $eq: [{ $type: "$type" }, "string"], // only scalar string, not existing array + }, + }, + [ + { + $set: { + type: ["$type"], + }, + }, + ] + ); + + if (result.modifiedCount === 0) { + logger.info({ service: SERVICE_NAME, message: "No StatusPage documents needed migration" }); + return; + } + + logger.info({ + service: SERVICE_NAME, + message: `Migration complete. Converted ${result.modifiedCount} StatusPage document(s) from string type to array type`, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error({ service: SERVICE_NAME, message: `Error during StatusPage type migration: ${errorMessage}` }); + throw error; + } +} diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts index 3b7b45ada..61f16ed4d 100644 --- a/server/src/db/migration/index.ts +++ b/server/src/db/migration/index.ts @@ -3,6 +3,7 @@ import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js"; import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js"; import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js"; import MigrationModel from "../models/Migration.js"; +import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js"; type MigrationEntry = { name: string; @@ -14,6 +15,7 @@ const migrations: MigrationEntry[] = [ { name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries }, { name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats }, { name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds }, + { name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray }, ]; const runMigrations = async (logger?: { info: Function; error: Function }) => { diff --git a/server/src/db/models/StatusPage.ts b/server/src/db/models/StatusPage.ts index 56c5ee299..e9b5b59aa 100644 --- a/server/src/db/models/StatusPage.ts +++ b/server/src/db/models/StatusPage.ts @@ -43,9 +43,9 @@ const StatusPageSchema = new Schema( required: true, }, type: { - type: String, + type: [String], required: true, - default: "uptime", + default: ["uptime"], enum: StatusPageTypes, }, companyName: { @@ -100,6 +100,10 @@ const StatusPageSchema = new Schema( type: Boolean, default: false, }, + showInfrastructure: { + type: Boolean, + default: false, + }, customCSS: { type: String, default: "", diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts index e19676285..eb2d92729 100644 --- a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -57,6 +57,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { showCharts: doc.showCharts, showUptimePercentage: doc.showUptimePercentage, showAdminLoginLink: doc.showAdminLoginLink, + showInfrastructure: doc.showInfrastructure, customCSS: doc.customCSS, createdAt: this.toDateString(doc.createdAt), updatedAt: this.toDateString(doc.updatedAt), diff --git a/server/src/types/statusPage.ts b/server/src/types/statusPage.ts index b11cf53d6..0329f4e6f 100644 --- a/server/src/types/statusPage.ts +++ b/server/src/types/statusPage.ts @@ -1,4 +1,4 @@ -export const StatusPageTypes = ["uptime"] as const; +export const StatusPageTypes = ["uptime", "infrastructure"] as const; export type StatusPageType = (typeof StatusPageTypes)[number]; export interface StatusPageLogo { @@ -15,7 +15,7 @@ export interface StatusPage { id: string; userId: string; teamId: string; - type: StatusPageType; + type: StatusPageType[]; companyName: string; url: string; timezone?: string; @@ -28,6 +28,7 @@ export interface StatusPage { showCharts: boolean; showUptimePercentage: boolean; showAdminLoginLink: boolean; + showInfrastructure: boolean; customCSS: string; createdAt: string; updatedAt: string; diff --git a/server/src/validation/statusPageValidation.ts b/server/src/validation/statusPageValidation.ts index b4b5e270c..8b2ab3f20 100644 --- a/server/src/validation/statusPageValidation.ts +++ b/server/src/validation/statusPageValidation.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; +import { StatusPageTypes } from "@/types/statusPage.js"; //**************************************** // Status Page Validations @@ -10,13 +11,13 @@ export const getStatusPageParamValidation = z.object({ }); export const getStatusPageQueryValidation = z.object({ - type: z.literal("uptime"), + type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])), timeFrame: z.coerce.number().optional(), }); export const createStatusPageBodyValidation = z .object({ - type: z.literal("uptime"), + type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])), companyName: z.string().min(1, "Company name is required"), url: z.string().regex(/^[a-zA-Z0-9_-]+$/, { message: "URL can only contain letters, numbers, underscores, and hyphens", @@ -30,6 +31,7 @@ export const createStatusPageBodyValidation = z showCharts: booleanCoercion.optional(), showUptimePercentage: booleanCoercion, showAdminLoginLink: booleanCoercion.optional(), + showInfrastructure: booleanCoercion.optional(), removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(), }) .strip();