diff --git a/migrations/20250111153517_init.ts b/migrations/20250111153517_init.ts index 0e0c50c2..50a04bbc 100644 --- a/migrations/20250111153517_init.ts +++ b/migrations/20250111153517_init.ts @@ -1,17 +1,19 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - await knex.schema - .createTable("monitoring_data", (table) => { + if (!(await knex.schema.hasTable("monitoring_data"))) { + await knex.schema.createTable("monitoring_data", (table) => { table.string("monitor_tag", 255).notNullable(); table.integer("timestamp").notNullable(); table.text("status"); table.float("latency", 8, 2); table.text("type"); table.primary(["monitor_tag", "timestamp"]); - }) - // Create monitor_alerts table - .createTable("monitor_alerts", (table) => { + }); + } + + if (!(await knex.schema.hasTable("monitor_alerts"))) { + await knex.schema.createTable("monitor_alerts", (table) => { table.increments("id").primary(); table.string("monitor_tag", 255).notNullable(); table.string("monitor_status", 255).notNullable(); @@ -20,18 +22,29 @@ export async function up(knex: Knex): Promise { table.integer("incident_number").defaultTo(0); table.timestamp("created_at").defaultTo(knex.fn.now()); table.timestamp("updated_at").defaultTo(knex.fn.now()); - }) - // Add index to monitor_alerts table - .raw("CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)") - .createTable("site_data", (table) => { + }); + } + + // Add index (IF NOT EXISTS not supported by all DBs, so use try/catch) + try { + await knex.schema.raw("CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)"); + } catch (_e) { + // Index already exists + } + + if (!(await knex.schema.hasTable("site_data"))) { + await knex.schema.createTable("site_data", (table) => { table.increments("id").primary(); table.string("key", 255).notNullable().unique(); table.text("value").notNullable(); table.string("data_type", 255).notNullable(); table.timestamp("created_at").defaultTo(knex.fn.now()); table.timestamp("updated_at").defaultTo(knex.fn.now()); - }) - .createTable("monitors", (table) => { + }); + } + + if (!(await knex.schema.hasTable("monitors"))) { + await knex.schema.createTable("monitors", (table) => { table.increments("id").primary(); table.string("tag", 255).notNullable().unique(); table.string("name", 255).notNullable().unique(); @@ -50,8 +63,11 @@ export async function up(knex: Knex): Promise { table.string("include_degraded_in_downtime", 255).defaultTo("NO"); table.timestamp("created_at").defaultTo(knex.fn.now()); table.timestamp("updated_at").defaultTo(knex.fn.now()); - }) - .createTable("triggers", (table) => { + }); + } + + if (!(await knex.schema.hasTable("triggers"))) { + await knex.schema.createTable("triggers", (table) => { table.increments("id").primary(); table.string("name", 255).notNullable().unique(); table.string("trigger_type", 255); @@ -60,8 +76,11 @@ export async function up(knex: Knex): Promise { table.text("trigger_meta"); table.timestamp("created_at").defaultTo(knex.fn.now()); table.timestamp("updated_at").defaultTo(knex.fn.now()); - }) - .createTable("users", (table) => { + }); + } + + if (!(await knex.schema.hasTable("users"))) { + await knex.schema.createTable("users", (table) => { table.increments("id").primary(); table.string("email", 255).notNullable().unique(); table.string("name", 255).notNullable(); @@ -71,8 +90,11 @@ export async function up(knex: Knex): Promise { table.string("role", 255).defaultTo("user"); table.timestamp("created_at").defaultTo(knex.fn.now()); table.timestamp("updated_at").defaultTo(knex.fn.now()); - }) - .createTable("api_keys", (table) => { + }); + } + + if (!(await knex.schema.hasTable("api_keys"))) { + await knex.schema.createTable("api_keys", (table) => { table.increments("id").primary(); table.string("name", 255).notNullable().unique(); table.string("hashed_key", 255).notNullable().unique(); @@ -80,8 +102,11 @@ export async function up(knex: Knex): Promise { table.string("status", 255).defaultTo("ACTIVE"); table.timestamp("created_at").defaultTo(knex.fn.now()); table.timestamp("updated_at").defaultTo(knex.fn.now()); - }) - .createTable("incidents", (table) => { + }); + } + + if (!(await knex.schema.hasTable("incidents"))) { + await knex.schema.createTable("incidents", (table) => { table.increments("id").primary(); table.string("title", 255).notNullable(); table.integer("start_date_time").notNullable(); @@ -90,8 +115,11 @@ export async function up(knex: Knex): Promise { table.timestamp("updated_at").defaultTo(knex.fn.now()); table.string("status", 255).defaultTo("ACTIVE"); table.string("state", 255).defaultTo("INVESTIGATING"); - }) - .createTable("incident_monitors", (table) => { + }); + } + + if (!(await knex.schema.hasTable("incident_monitors"))) { + await knex.schema.createTable("incident_monitors", (table) => { table.increments("id").primary(); table.string("monitor_tag", 255).notNullable(); table.string("monitor_impact", 255); @@ -99,8 +127,11 @@ export async function up(knex: Knex): Promise { table.timestamp("updated_at").defaultTo(knex.fn.now()); table.integer("incident_id").notNullable(); table.unique(["monitor_tag", "incident_id"]); - }) - .createTable("incident_comments", (table) => { + }); + } + + if (!(await knex.schema.hasTable("incident_comments"))) { + await knex.schema.createTable("incident_comments", (table) => { table.increments("id").primary(); table.text("comment").notNullable(); table.integer("incident_id").notNullable(); @@ -110,6 +141,7 @@ export async function up(knex: Knex): Promise { table.string("status", 255).defaultTo("ACTIVE"); table.string("state", 255).defaultTo("INVESTIGATING"); }); + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20250201152526_add_new_column_to_incidents.ts b/migrations/20250201152526_add_new_column_to_incidents.ts index afbb879c..c5c306b5 100644 --- a/migrations/20250201152526_add_new_column_to_incidents.ts +++ b/migrations/20250201152526_add_new_column_to_incidents.ts @@ -1,9 +1,12 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - await knex.schema.alterTable("incidents", function (table) { - table.text("incident_type").defaultTo("INCIDENT"); - }); + const hasCol = await knex.schema.hasColumn("incidents", "incident_type"); + if (!hasCol) { + await knex.schema.alterTable("incidents", function (table) { + table.text("incident_type").defaultTo("INCIDENT"); + }); + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20250222153624_add_incident_source_column.ts b/migrations/20250222153624_add_incident_source_column.ts index 9afb8422..11802f0f 100644 --- a/migrations/20250222153624_add_incident_source_column.ts +++ b/migrations/20250222153624_add_incident_source_column.ts @@ -1,10 +1,12 @@ import type { Knex } from "knex"; - export async function up(knex: Knex): Promise { - await knex.schema.alterTable("incidents", function (table) { - table.text("incident_source").defaultTo("DASHBOARD"); - }); + const hasCol = await knex.schema.hasColumn("incidents", "incident_source"); + if (!hasCol) { + await knex.schema.alterTable("incidents", function (table) { + table.text("incident_source").defaultTo("DASHBOARD"); + }); + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20250315091330_add_table_invitations.ts b/migrations/20250315091330_add_table_invitations.ts index ac6c3cd0..c578fd0f 100644 --- a/migrations/20250315091330_add_table_invitations.ts +++ b/migrations/20250315091330_add_table_invitations.ts @@ -1,6 +1,8 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable("invitations")) return; + await knex.schema.createTable("invitations", (table) => { // Primary key table.increments("id").primary(); diff --git a/migrations/20250417144954_add_subscription_tables.ts b/migrations/20250417144954_add_subscription_tables.ts index d751d0c0..8a5f7b9d 100644 --- a/migrations/20250417144954_add_subscription_tables.ts +++ b/migrations/20250417144954_add_subscription_tables.ts @@ -1,8 +1,8 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - await knex.schema - .createTable("subscribers", (table) => { + if (!(await knex.schema.hasTable("subscribers"))) { + await knex.schema.createTable("subscribers", (table) => { table.increments("id").primary(); table.string("subscriber_send").notNullable(); table.text("subscriber_meta").nullable(); @@ -16,8 +16,11 @@ export async function up(knex: Knex): Promise { // Add index on subscriber_send for better query performance table.index(["subscriber_send"]); - }) - .createTable("subscriptions", (table) => { + }); + } + + if (!(await knex.schema.hasTable("subscriptions"))) { + await knex.schema.createTable("subscriptions", (table) => { table.increments("id").primary(); table.integer("subscriber_id").unsigned().notNullable(); table.string("subscriptions_status").notNullable(); @@ -32,8 +35,11 @@ export async function up(knex: Knex): Promise { // Add index to optimize queries filtering by status and monitors table.index(["subscriptions_status", "subscriptions_monitors"]); - }) - .createTable("subscription_triggers", (table) => { + }); + } + + if (!(await knex.schema.hasTable("subscription_triggers"))) { + await knex.schema.createTable("subscription_triggers", (table) => { table.increments("id").primary(); table.string("subscription_trigger_type").notNullable().unique(); table.string("subscription_trigger_status").notNullable(); @@ -41,6 +47,7 @@ export async function up(knex: Knex): Promise { table.datetime("created_at").defaultTo(knex.fn.now()); table.datetime("updated_at").defaultTo(knex.fn.now()); }); + } } export async function down(knex: Knex): Promise { await knex.schema diff --git a/migrations/20260106120000_add_images_table.ts b/migrations/20260106120000_add_images_table.ts index 0b99af47..9920ada5 100644 --- a/migrations/20260106120000_add_images_table.ts +++ b/migrations/20260106120000_add_images_table.ts @@ -1,6 +1,8 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable("images")) return; + await knex.schema.createTable("images", (table) => { table.string("id", 32).primary(); // nanoid generated ID with prefix table.text("data").notNullable(); // base64 encoded image data diff --git a/migrations/20260107120000_add_pages_tables.ts b/migrations/20260107120000_add_pages_tables.ts index 09f69965..046f9101 100644 --- a/migrations/20260107120000_add_pages_tables.ts +++ b/migrations/20260107120000_add_pages_tables.ts @@ -2,37 +2,49 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { // Create pages table - await knex.schema.createTable("pages", (table) => { - table.increments("id").primary(); - table.string("page_path", 255).notNullable().unique(); // e.g., "/", "/api", "/infrastructure" - table.string("page_title", 255).notNullable(); - table.string("page_header", 255); - table.string("page_subheader", 255); - table.string("page_logo", 255); - table.text("page_settings_json"); // JSON settings for the page - table.timestamp("created_at").defaultTo(knex.fn.now()); - table.timestamp("updated_at").defaultTo(knex.fn.now()); - }); + if (!(await knex.schema.hasTable("pages"))) { + await knex.schema.createTable("pages", (table) => { + table.increments("id").primary(); + table.string("page_path", 255).notNullable().unique(); // e.g., "/", "/api", "/infrastructure" + table.string("page_title", 255).notNullable(); + table.string("page_header", 255); + table.string("page_subheader", 255); + table.string("page_logo", 255); + table.text("page_settings_json"); // JSON settings for the page + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); + }); + } // Create pages_monitors junction table - await knex.schema.createTable("pages_monitors", (table) => { - table.integer("page_id").unsigned().notNullable(); - table.string("monitor_tag", 255).notNullable(); - table.text("monitor_settings_json"); // JSON settings for monitor on this page (e.g., order, visibility) - table.timestamp("created_at").defaultTo(knex.fn.now()); - table.timestamp("updated_at").defaultTo(knex.fn.now()); + if (!(await knex.schema.hasTable("pages_monitors"))) { + await knex.schema.createTable("pages_monitors", (table) => { + table.integer("page_id").unsigned().notNullable(); + table.string("monitor_tag", 255).notNullable(); + table.text("monitor_settings_json"); // JSON settings for monitor on this page (e.g., order, visibility) + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); - // Composite primary key - table.primary(["page_id", "monitor_tag"]); + // Composite primary key + table.primary(["page_id", "monitor_tag"]); - // Foreign key constraints - table.foreign("page_id").references("id").inTable("pages").onDelete("CASCADE"); - table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE"); - }); + // Foreign key constraints + table.foreign("page_id").references("id").inTable("pages").onDelete("CASCADE"); + table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE"); + }); + } - // Add index for faster lookups - await knex.schema.raw("CREATE INDEX idx_pages_monitors_page_id ON pages_monitors (page_id)"); - await knex.schema.raw("CREATE INDEX idx_pages_monitors_monitor_tag ON pages_monitors (monitor_tag)"); + // Add indexes (safe to fail if they already exist) + try { + await knex.schema.raw("CREATE INDEX idx_pages_monitors_page_id ON pages_monitors (page_id)"); + } catch (_e) { + /* index already exists */ + } + try { + await knex.schema.raw("CREATE INDEX idx_pages_monitors_monitor_tag ON pages_monitors (monitor_tag)"); + } catch (_e) { + /* index already exists */ + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20260109120000_add_maintenances_tables.ts b/migrations/20260109120000_add_maintenances_tables.ts index 4de88a69..75e24429 100644 --- a/migrations/20260109120000_add_maintenances_tables.ts +++ b/migrations/20260109120000_add_maintenances_tables.ts @@ -1,64 +1,67 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - // Create maintenances table - defines maintenance schedules using iCalendar RRULE - // RRULE examples: - // - ONE_TIME: FREQ=MINUTELY;COUNT=1 (single occurrence) - // - RECURRING: FREQ=WEEKLY;BYDAY=SU;BYHOUR=2;BYMINUTE=0 (every Sunday at 2 AM) - // Reference: http://www.kanzaki.com/docs/ical/rrule.html - await knex.schema.createTable("maintenances", (table) => { - table.increments("id").primary(); - table.string("title", 255).notNullable(); - table.text("description").nullable(); // Maintenance details/description - table.integer("start_date_time").notNullable(); // Unix timestamp - when the first occurrence starts - table.string("rrule", 500).notNullable(); // iCalendar RRULE string (e.g., FREQ=WEEKLY;BYDAY=SU) - table.integer("duration_seconds").notNullable(); // Duration of each maintenance window in seconds - table.string("status", 50).notNullable().defaultTo("ACTIVE"); // ACTIVE or INACTIVE - table.timestamp("created_at").defaultTo(knex.fn.now()); - table.timestamp("updated_at").defaultTo(knex.fn.now()); - }); + if (!(await knex.schema.hasTable("maintenances"))) { + await knex.schema.createTable("maintenances", (table) => { + table.increments("id").primary(); + table.string("title", 255).notNullable(); + table.text("description").nullable(); + table.integer("start_date_time").notNullable(); + table.string("rrule", 500).notNullable(); + table.integer("duration_seconds").notNullable(); + table.string("status", 50).notNullable().defaultTo("ACTIVE"); + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); + }); + } - // Create maintenance_monitors junction table - links monitors to maintenance schedules - await knex.schema.createTable("maintenance_monitors", (table) => { - table.increments("id").primary(); - table.integer("maintenance_id").unsigned().notNullable(); - table.string("monitor_tag", 255).notNullable(); - table.timestamp("created_at").defaultTo(knex.fn.now()); - table.timestamp("updated_at").defaultTo(knex.fn.now()); + if (!(await knex.schema.hasTable("maintenance_monitors"))) { + await knex.schema.createTable("maintenance_monitors", (table) => { + table.increments("id").primary(); + table.integer("maintenance_id").unsigned().notNullable(); + table.string("monitor_tag", 255).notNullable(); + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); - // Foreign key constraints - table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE"); - table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE"); + table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE"); + table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE"); - // Unique constraint to prevent duplicate monitor assignments - table.unique(["maintenance_id", "monitor_tag"]); - }); + table.unique(["maintenance_id", "monitor_tag"]); + }); + } - // Create maintenances_events table - actual maintenance occurrences (generated by job) - await knex.schema.createTable("maintenances_events", (table) => { - table.increments("id").primary(); - table.integer("maintenance_id").unsigned().notNullable(); - table.integer("start_date_time").notNullable(); // Unix timestamp - table.integer("end_date_time").notNullable(); // Unix timestamp - table.string("status", 50).notNullable().defaultTo("SCHEDULED"); // SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED - table.timestamp("created_at").defaultTo(knex.fn.now()); - table.timestamp("updated_at").defaultTo(knex.fn.now()); + if (!(await knex.schema.hasTable("maintenances_events"))) { + await knex.schema.createTable("maintenances_events", (table) => { + table.increments("id").primary(); + table.integer("maintenance_id").unsigned().notNullable(); + table.integer("start_date_time").notNullable(); + table.integer("end_date_time").notNullable(); + table.string("status", 50).notNullable().defaultTo("SCHEDULED"); + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); - // Foreign key constraint - table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE"); - }); + table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE"); + }); + } - // Add indexes for faster lookups - await knex.schema.raw("CREATE INDEX idx_maintenances_status ON maintenances (status)"); - await knex.schema.raw("CREATE INDEX idx_maintenances_start_time ON maintenances (start_date_time)"); - await knex.schema.raw( + // Add indexes (safe to fail if they already exist) + const indexes = [ + "CREATE INDEX idx_maintenances_status ON maintenances (status)", + "CREATE INDEX idx_maintenances_start_time ON maintenances (start_date_time)", "CREATE INDEX idx_maintenance_monitors_maintenance_id ON maintenance_monitors (maintenance_id)", - ); - await knex.schema.raw("CREATE INDEX idx_maintenance_monitors_monitor_tag ON maintenance_monitors (monitor_tag)"); - await knex.schema.raw("CREATE INDEX idx_maintenances_events_maintenance_id ON maintenances_events (maintenance_id)"); - await knex.schema.raw("CREATE INDEX idx_maintenances_events_status ON maintenances_events (status)"); - await knex.schema.raw("CREATE INDEX idx_maintenances_events_start_time ON maintenances_events (start_date_time)"); - await knex.schema.raw("CREATE INDEX idx_maintenances_events_end_time ON maintenances_events (end_date_time)"); + "CREATE INDEX idx_maintenance_monitors_monitor_tag ON maintenance_monitors (monitor_tag)", + "CREATE INDEX idx_maintenances_events_maintenance_id ON maintenances_events (maintenance_id)", + "CREATE INDEX idx_maintenances_events_status ON maintenances_events (status)", + "CREATE INDEX idx_maintenances_events_start_time ON maintenances_events (start_date_time)", + "CREATE INDEX idx_maintenances_events_end_time ON maintenances_events (end_date_time)", + ]; + for (const sql of indexes) { + try { + await knex.schema.raw(sql); + } catch (_e) { + /* index already exists */ + } + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20260114120000_remove_monitors_name_unique.ts b/migrations/20260114120000_remove_monitors_name_unique.ts index 325d585e..c2751b76 100644 --- a/migrations/20260114120000_remove_monitors_name_unique.ts +++ b/migrations/20260114120000_remove_monitors_name_unique.ts @@ -1,10 +1,14 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - // Remove unique constraint from monitors.name - await knex.schema.alterTable("monitors", (table) => { - table.dropUnique(["name"]); - }); + // Remove unique constraint from monitors.name (safe to fail if already dropped) + try { + await knex.schema.alterTable("monitors", (table) => { + table.dropUnique(["name"]); + }); + } catch (_e) { + // Constraint already removed + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20260117120000_add_is_hidden_to_monitors.ts b/migrations/20260117120000_add_is_hidden_to_monitors.ts index c4e4a49f..0724f23a 100644 --- a/migrations/20260117120000_add_is_hidden_to_monitors.ts +++ b/migrations/20260117120000_add_is_hidden_to_monitors.ts @@ -1,9 +1,12 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - await knex.schema.alterTable("monitors", (table) => { - table.string("is_hidden").defaultTo("NO").notNullable(); - }); + const hasCol = await knex.schema.hasColumn("monitors", "is_hidden"); + if (!hasCol) { + await knex.schema.alterTable("monitors", (table) => { + table.string("is_hidden").defaultTo("NO").notNullable(); + }); + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20260120120000_add_monitor_settings_json.ts b/migrations/20260120120000_add_monitor_settings_json.ts index 2f399552..b46e57a7 100644 --- a/migrations/20260120120000_add_monitor_settings_json.ts +++ b/migrations/20260120120000_add_monitor_settings_json.ts @@ -1,9 +1,12 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - await knex.schema.alterTable("monitors", (table) => { - table.text("monitor_settings_json").nullable(); - }); + const hasCol = await knex.schema.hasColumn("monitors", "monitor_settings_json"); + if (!hasCol) { + await knex.schema.alterTable("monitors", (table) => { + table.text("monitor_settings_json").nullable(); + }); + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20260123110239_maintenace_monitor_status.ts b/migrations/20260123110239_maintenace_monitor_status.ts index c307c074..62cc45cb 100644 --- a/migrations/20260123110239_maintenace_monitor_status.ts +++ b/migrations/20260123110239_maintenace_monitor_status.ts @@ -1,9 +1,12 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - await knex.schema.alterTable("maintenance_monitors", (table) => { - table.string("monitor_impact").defaultTo("MAINTENANCE").notNullable(); - }); + const hasCol = await knex.schema.hasColumn("maintenance_monitors", "monitor_impact"); + if (!hasCol) { + await knex.schema.alterTable("maintenance_monitors", (table) => { + table.string("monitor_impact").defaultTo("MAINTENANCE").notNullable(); + }); + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20260124120000_add_monitor_alerts_config_tables.ts b/migrations/20260124120000_add_monitor_alerts_config_tables.ts index af358c34..59c3c6a0 100644 --- a/migrations/20260124120000_add_monitor_alerts_config_tables.ts +++ b/migrations/20260124120000_add_monitor_alerts_config_tables.ts @@ -2,42 +2,54 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { // Create monitor_alerts_config table - await knex.schema.createTable("monitor_alerts_config", (table) => { - table.increments("id").primary(); - table.string("monitor_tag", 255).notNullable(); - table.string("alert_for", 50).notNullable(); // STATUS, LATENCY, UPTIME - table.string("alert_value", 255).notNullable(); // DOWN, DEGRADED, or numeric value like "1000" or "99" - table.integer("failure_threshold").notNullable().defaultTo(1); - table.integer("success_threshold").notNullable().defaultTo(1); - table.text("alert_description"); - table.string("create_incident", 10).notNullable().defaultTo("NO"); // YES or NO - table.string("is_active", 10).notNullable().defaultTo("YES"); // YES or NO - table.string("severity", 50).notNullable().defaultTo("WARNING"); // CRITICAL or WARNING - table.timestamp("created_at").defaultTo(knex.fn.now()); - table.timestamp("updated_at").defaultTo(knex.fn.now()); + if (!(await knex.schema.hasTable("monitor_alerts_config"))) { + await knex.schema.createTable("monitor_alerts_config", (table) => { + table.increments("id").primary(); + table.string("monitor_tag", 255).notNullable(); + table.string("alert_for", 50).notNullable(); // STATUS, LATENCY, UPTIME + table.string("alert_value", 255).notNullable(); // DOWN, DEGRADED, or numeric value like "1000" or "99" + table.integer("failure_threshold").notNullable().defaultTo(1); + table.integer("success_threshold").notNullable().defaultTo(1); + table.text("alert_description"); + table.string("create_incident", 10).notNullable().defaultTo("NO"); // YES or NO + table.string("is_active", 10).notNullable().defaultTo("YES"); // YES or NO + table.string("severity", 50).notNullable().defaultTo("WARNING"); // CRITICAL or WARNING + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); - // Foreign key to monitors table - table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE"); - }); + // Foreign key to monitors table + table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE"); + }); + } - // Create index for faster lookups - await knex.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)"); - await knex.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)"); + // Create indexes (safe to fail if they already exist) + try { + await knex.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)"); + } catch (_e) { + /* index already exists */ + } + try { + await knex.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)"); + } catch (_e) { + /* index already exists */ + } // Create monitor_alerts_config_triggers junction table - await knex.schema.createTable("monitor_alerts_config_triggers", (table) => { - table.integer("monitor_alerts_id").unsigned().notNullable(); - table.integer("trigger_id").unsigned().notNullable(); - table.timestamp("created_at").defaultTo(knex.fn.now()); - table.timestamp("updated_at").defaultTo(knex.fn.now()); + if (!(await knex.schema.hasTable("monitor_alerts_config_triggers"))) { + await knex.schema.createTable("monitor_alerts_config_triggers", (table) => { + table.integer("monitor_alerts_id").unsigned().notNullable(); + table.integer("trigger_id").unsigned().notNullable(); + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); - // Composite primary key - table.primary(["monitor_alerts_id", "trigger_id"]); + // Composite primary key + table.primary(["monitor_alerts_id", "trigger_id"]); - // Foreign keys - table.foreign("monitor_alerts_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE"); - table.foreign("trigger_id").references("id").inTable("triggers").onDelete("CASCADE"); - }); + // Foreign keys + table.foreign("monitor_alerts_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE"); + table.foreign("trigger_id").references("id").inTable("triggers").onDelete("CASCADE"); + }); + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20260124120001_monitor_alert_config_id.ts b/migrations/20260124120001_monitor_alert_config_id.ts index 59a6e794..2ae663f6 100644 --- a/migrations/20260124120001_monitor_alert_config_id.ts +++ b/migrations/20260124120001_monitor_alert_config_id.ts @@ -1,6 +1,8 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable("monitor_alerts_v2")) return; + await knex.schema.createTable("monitor_alerts_v2", (table) => { table.increments("id").primary(); table.integer("config_id").references("id").inTable("monitor_alerts_config").notNullable().onDelete("CASCADE"); @@ -15,5 +17,5 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - await knex.schema.dropTable("monitor_alerts_v2"); + await knex.schema.dropTableIfExists("monitor_alerts_v2"); } diff --git a/migrations/20260128120000_redesign_subscription_system.ts b/migrations/20260128120000_redesign_subscription_system.ts index f6b07250..844aa6af 100644 --- a/migrations/20260128120000_redesign_subscription_system.ts +++ b/migrations/20260128120000_redesign_subscription_system.ts @@ -2,93 +2,58 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { // 1. Create subscriber_users table - the actual user identity - await knex.schema.createTable("subscriber_users", (table) => { - table.increments("id").primary(); + if (!(await knex.schema.hasTable("subscriber_users"))) { + await knex.schema.createTable("subscriber_users", (table) => { + table.increments("id").primary(); + table.string("email", 255).notNullable().unique(); + table.string("status", 20).notNullable().defaultTo("PENDING"); + table.string("verification_code", 10).nullable(); + table.timestamp("verification_expires_at").nullable(); + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); + table.index(["status"]); + table.index(["email"]); + }); + } - // Email is the primary identifier for users - table.string("email", 255).notNullable().unique(); + // 2. Create subscriber_methods table + if (!(await knex.schema.hasTable("subscriber_methods"))) { + await knex.schema.createTable("subscriber_methods", (table) => { + table.increments("id").primary(); + table.integer("subscriber_user_id").unsigned().notNullable(); + table.string("method_type", 50).notNullable(); + table.string("method_value", 500).notNullable(); + table.string("status", 20).notNullable().defaultTo("ACTIVE"); + table.text("meta").nullable(); + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); + table.index(["subscriber_user_id"]); + table.index(["method_type"]); + table.index(["status"]); + table.unique(["subscriber_user_id", "method_type", "method_value"]); + table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE"); + }); + } - // User status: PENDING (awaiting verification), ACTIVE, INACTIVE - table.string("status", 20).notNullable().defaultTo("PENDING"); - - // Verification code for email verification (6 digit) - table.string("verification_code", 10).nullable(); - table.timestamp("verification_expires_at").nullable(); - - table.timestamp("created_at").defaultTo(knex.fn.now()); - table.timestamp("updated_at").defaultTo(knex.fn.now()); - - // Indexes - table.index(["status"]); - table.index(["email"]); - }); - - // 2. Create subscriber_methods table - methods a user has configured - await knex.schema.createTable("subscriber_methods", (table) => { - table.increments("id").primary(); - - // Link to subscriber_user - table.integer("subscriber_user_id").unsigned().notNullable(); - - // Method type: email, webhook, slack, discord - table.string("method_type", 50).notNullable(); - - // Method value: email address, webhook URL, slack webhook, discord webhook - table.string("method_value", 500).notNullable(); - - // Status: ACTIVE, INACTIVE - table.string("status", 20).notNullable().defaultTo("ACTIVE"); - - // For webhook methods, we might want to store additional config - table.text("meta").nullable(); // JSON for extra config like headers - - table.timestamp("created_at").defaultTo(knex.fn.now()); - table.timestamp("updated_at").defaultTo(knex.fn.now()); - - // Indexes - table.index(["subscriber_user_id"]); - table.index(["method_type"]); - table.index(["status"]); - - // Unique: one method type per value per user (can't have same webhook twice) - table.unique(["subscriber_user_id", "method_type", "method_value"]); - - // Foreign key - table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE"); - }); - - // 3. Create user_subscriptions_v2 table - what a user subscribes to - await knex.schema.createTable("user_subscriptions_v2", (table) => { - table.increments("id").primary(); - - // Link to subscriber_user - table.integer("subscriber_user_id").unsigned().notNullable(); - - // Link to subscriber_method (which method to use for this subscription) - table.integer("subscriber_method_id").unsigned().notNullable(); - - // What event type: incidents, maintenance - table.string("event_type", 50).notNullable(); - - // Status: ACTIVE, INACTIVE - table.string("status", 20).notNullable().defaultTo("ACTIVE"); - - table.timestamp("created_at").defaultTo(knex.fn.now()); - table.timestamp("updated_at").defaultTo(knex.fn.now()); - - // Indexes - table.index(["subscriber_user_id"]); - table.index(["subscriber_method_id"]); - table.index(["event_type"]); - table.index(["status"]); - - // Unique: one subscription per user-method-event-entity - table.unique(["subscriber_user_id", "subscriber_method_id", "event_type"]); - - // Foreign keys - table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE"); - table.foreign("subscriber_method_id").references("id").inTable("subscriber_methods").onDelete("CASCADE"); - }); + // 3. Create user_subscriptions_v2 table + if (!(await knex.schema.hasTable("user_subscriptions_v2"))) { + await knex.schema.createTable("user_subscriptions_v2", (table) => { + table.increments("id").primary(); + table.integer("subscriber_user_id").unsigned().notNullable(); + table.integer("subscriber_method_id").unsigned().notNullable(); + table.string("event_type", 50).notNullable(); + table.string("status", 20).notNullable().defaultTo("ACTIVE"); + table.timestamp("created_at").defaultTo(knex.fn.now()); + table.timestamp("updated_at").defaultTo(knex.fn.now()); + table.index(["subscriber_user_id"]); + table.index(["subscriber_method_id"]); + table.index(["event_type"]); + table.index(["status"]); + table.unique(["subscriber_user_id", "subscriber_method_id", "event_type"]); + table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE"); + table.foreign("subscriber_method_id").references("id").inTable("subscriber_methods").onDelete("CASCADE"); + }); + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20260128145145_add_general_email_templates.ts b/migrations/20260128145145_add_general_email_templates.ts index 5c7afa7d..e505c24e 100644 --- a/migrations/20260128145145_add_general_email_templates.ts +++ b/migrations/20260128145145_add_general_email_templates.ts @@ -1,6 +1,8 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { + if (await knex.schema.hasTable("general_email_templates")) return; + await knex.schema.createTable("general_email_templates", (table) => { table.string("template_id").primary(); table.string("template_subject"); diff --git a/migrations/20260202090915_add_new_columns_data_monitors.ts b/migrations/20260202090915_add_new_columns_data_monitors.ts index 573effe3..2ed6022c 100644 --- a/migrations/20260202090915_add_new_columns_data_monitors.ts +++ b/migrations/20260202090915_add_new_columns_data_monitors.ts @@ -1,12 +1,16 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - await knex.schema.alterTable("monitors", (table) => { - table.text("external_url").nullable(); - }); - await knex.schema.alterTable("monitoring_data", (table) => { - table.text("error_message").nullable(); - }); + if (!(await knex.schema.hasColumn("monitors", "external_url"))) { + await knex.schema.alterTable("monitors", (table) => { + table.text("external_url").nullable(); + }); + } + if (!(await knex.schema.hasColumn("monitoring_data", "error_message"))) { + await knex.schema.alterTable("monitoring_data", (table) => { + table.text("error_message").nullable(); + }); + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20260216095021_index_monitoring_data.ts b/migrations/20260216095021_index_monitoring_data.ts index 90a4a4f2..c3a19b51 100644 --- a/migrations/20260216095021_index_monitoring_data.ts +++ b/migrations/20260216095021_index_monitoring_data.ts @@ -1,10 +1,20 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - await knex.schema.alterTable("monitoring_data", (table) => { - table.index(["timestamp"], "idx_monitoring_data_timestamp"); - table.index(["monitor_tag", "type", "timestamp"], "idx_monitoring_data_monitor_tag_type_timestamp"); - }); + try { + await knex.schema.alterTable("monitoring_data", (table) => { + table.index(["timestamp"], "idx_monitoring_data_timestamp"); + }); + } catch (_e) { + /* index already exists */ + } + try { + await knex.schema.alterTable("monitoring_data", (table) => { + table.index(["monitor_tag", "type", "timestamp"], "idx_monitoring_data_monitor_tag_type_timestamp"); + }); + } catch (_e) { + /* index already exists */ + } } export async function down(knex: Knex): Promise { diff --git a/migrations/20260219165446_add_global_colum_incidents.ts b/migrations/20260219165446_add_global_colum_incidents.ts index 721c80e7..46f2ffe1 100644 --- a/migrations/20260219165446_add_global_colum_incidents.ts +++ b/migrations/20260219165446_add_global_colum_incidents.ts @@ -1,12 +1,16 @@ import type { Knex } from "knex"; export async function up(knex: Knex): Promise { - await knex.schema.table("incidents", (table) => { - table.string("is_global", 15).notNullable().defaultTo("YES"); - }); - await knex.schema.table("maintenances", (table) => { - table.string("is_global", 15).notNullable().defaultTo("YES"); - }); + if (!(await knex.schema.hasColumn("incidents", "is_global"))) { + await knex.schema.table("incidents", (table) => { + table.string("is_global", 15).notNullable().defaultTo("YES"); + }); + } + if (!(await knex.schema.hasColumn("maintenances", "is_global"))) { + await knex.schema.table("maintenances", (table) => { + table.string("is_global", 15).notNullable().defaultTo("YES"); + }); + } } export async function down(knex: Knex): Promise {