mirror of
https://github.com/rajnandan1/kener.git
synced 2026-06-23 04:10:22 +00:00
216 lines
9.0 KiB
TypeScript
216 lines
9.0 KiB
TypeScript
/**
|
|
* Migration: Multi-Monitor Alerts
|
|
*
|
|
* This migration refactors the monitor alerting system to support many-to-many
|
|
* relationships between alert configurations and monitors. Previously, each
|
|
* `monitor_alerts_config` row was tied to exactly one monitor via a `monitor_tag`
|
|
* foreign key column. This migration:
|
|
*
|
|
* 1. Creates a new `monitor_alerts_config_monitors` junction table that links
|
|
* `monitor_alerts_config` rows to one or more `monitors` rows, enabling a
|
|
* single alert configuration to fire across multiple monitors.
|
|
*
|
|
* 2. Adds a `monitor_tag` column to `monitor_alerts_v2` so that each firing
|
|
* alert record knows which specific monitor triggered it (important when one
|
|
* config covers many monitors).
|
|
*
|
|
* 3. Migrates existing data: copies every `monitor_alerts_config.monitor_tag`
|
|
* value into the new junction table and backfills `monitor_alerts_v2.monitor_tag`
|
|
* from the same source, preserving all historical alert records.
|
|
*
|
|
* 4. Removes the one-to-one constraint on `monitor_alerts_config.monitor_tag` by
|
|
* dropping its foreign key and setting the column nullable (SQLite workaround:
|
|
* nulls the column directly since SQLite cannot drop foreign key constraints
|
|
* inline).
|
|
*
|
|
* 5. Adds a composite index on `monitor_alerts_v2 (config_id, monitor_tag,
|
|
* alert_status)` for fast per-monitor alert status lookups.
|
|
*
|
|
* The `down` migration reverses these steps: restores the first junction-table
|
|
* entry back onto `monitor_alerts_config.monitor_tag`, re-adds the foreign key
|
|
* (non-SQLite), removes the `monitor_tag` column from `monitor_alerts_v2`
|
|
* (non-SQLite), and drops the junction table.
|
|
*/
|
|
import type { Knex } from "knex";
|
|
|
|
export async function up(knex: Knex): Promise<void> {
|
|
// Step 1: Create monitor_alerts_config_monitors junction table
|
|
if (!(await knex.schema.hasTable("monitor_alerts_config_monitors"))) {
|
|
await knex.schema.createTable("monitor_alerts_config_monitors", (table) => {
|
|
table.integer("monitor_alerts_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());
|
|
|
|
// Composite primary key
|
|
table.primary(["monitor_alerts_id", "monitor_tag"]);
|
|
|
|
// Foreign keys
|
|
table.foreign("monitor_alerts_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
|
|
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
|
});
|
|
}
|
|
|
|
// Step 2: Add monitor_tag column to monitor_alerts_v2 for per-monitor alert tracking
|
|
const hasV2Column = await knex.schema.hasColumn("monitor_alerts_v2", "monitor_tag");
|
|
if (!hasV2Column) {
|
|
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
|
table.string("monitor_tag", 255).nullable();
|
|
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
|
});
|
|
}
|
|
|
|
// Step 3: Migrate existing data from monitor_alerts_config.monitor_tag to junction table
|
|
// and backfill monitor_alerts_v2.monitor_tag from the same source
|
|
const existingConfigs = await knex("monitor_alerts_config").whereNotNull("monitor_tag").select("id", "monitor_tag");
|
|
|
|
if (existingConfigs.length > 0) {
|
|
// Build a config_id -> monitor_tag map for backfilling alerts
|
|
const configTagMap = new Map<number, string>();
|
|
const inserts = existingConfigs.map((config: { id: number; monitor_tag: string }) => {
|
|
configTagMap.set(config.id, config.monitor_tag);
|
|
return {
|
|
monitor_alerts_id: config.id,
|
|
monitor_tag: config.monitor_tag,
|
|
created_at: knex.fn.now(),
|
|
updated_at: knex.fn.now(),
|
|
};
|
|
});
|
|
|
|
// Batch insert into junction table
|
|
const chunkSize = 100;
|
|
for (let i = 0; i < inserts.length; i += chunkSize) {
|
|
await knex("monitor_alerts_config_monitors").insert(inserts.slice(i, i + chunkSize));
|
|
}
|
|
|
|
// Backfill monitor_tag on existing monitor_alerts_v2 rows
|
|
const existingAlerts = await knex("monitor_alerts_v2").whereNull("monitor_tag").select("id", "config_id");
|
|
for (const alert of existingAlerts) {
|
|
const tag = configTagMap.get(alert.config_id);
|
|
if (tag) {
|
|
await knex("monitor_alerts_v2").where({ id: alert.id }).update({ monitor_tag: tag });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 4: Drop foreign key and make monitor_tag nullable on monitor_alerts_config
|
|
const dbClient = knex.client.config.client;
|
|
|
|
if (dbClient === "sqlite3" || dbClient === "better-sqlite3") {
|
|
// SQLite cannot ALTER COLUMN, so we rebuild the table with monitor_tag nullable
|
|
await knex.transaction(async (trx) => {
|
|
await trx.raw("PRAGMA foreign_keys = OFF");
|
|
await trx.raw("DROP TABLE IF EXISTS monitor_alerts_config_new");
|
|
await trx.raw(`
|
|
CREATE TABLE monitor_alerts_config_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
monitor_tag VARCHAR(255),
|
|
alert_for VARCHAR(50) NOT NULL,
|
|
alert_value VARCHAR(255) NOT NULL,
|
|
failure_threshold INTEGER NOT NULL DEFAULT 1,
|
|
success_threshold INTEGER NOT NULL DEFAULT 1,
|
|
alert_description TEXT,
|
|
create_incident VARCHAR(10) NOT NULL DEFAULT 'NO',
|
|
is_active VARCHAR(10) NOT NULL DEFAULT 'YES',
|
|
severity VARCHAR(50) NOT NULL DEFAULT 'WARNING',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
await trx.raw(`
|
|
INSERT INTO monitor_alerts_config_new
|
|
(id, monitor_tag, alert_for, alert_value, failure_threshold,
|
|
success_threshold, alert_description, create_incident,
|
|
is_active, severity, created_at, updated_at)
|
|
SELECT
|
|
id, NULL, alert_for, alert_value, failure_threshold,
|
|
success_threshold, alert_description, create_incident,
|
|
is_active, severity, created_at, updated_at
|
|
FROM monitor_alerts_config
|
|
`);
|
|
await trx.raw("DROP TABLE monitor_alerts_config");
|
|
await trx.raw("ALTER TABLE monitor_alerts_config_new RENAME TO monitor_alerts_config");
|
|
try {
|
|
await trx.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
|
|
} catch (_e) {
|
|
/* index may already exist */
|
|
}
|
|
try {
|
|
await trx.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
|
|
} catch (_e) {
|
|
/* index may already exist */
|
|
}
|
|
await trx.raw("PRAGMA foreign_keys = ON");
|
|
});
|
|
} else {
|
|
try {
|
|
await knex.schema.alterTable("monitor_alerts_config", (table) => {
|
|
table.dropForeign(["monitor_tag"]);
|
|
});
|
|
} catch (_e) {
|
|
// Foreign key may not exist or already dropped
|
|
}
|
|
|
|
await knex.schema.alterTable("monitor_alerts_config", (table) => {
|
|
table.string("monitor_tag", 255).nullable().alter();
|
|
});
|
|
|
|
await knex("monitor_alerts_config").update({ monitor_tag: null });
|
|
}
|
|
|
|
// Step 5: Add composite index on monitor_alerts_v2 for fast lookups
|
|
try {
|
|
await knex.raw(
|
|
"CREATE INDEX idx_monitor_alerts_v2_config_tag_status ON monitor_alerts_v2 (config_id, monitor_tag, alert_status)",
|
|
);
|
|
} catch (_e) {
|
|
/* index already exists */
|
|
}
|
|
}
|
|
|
|
export async function down(knex: Knex): Promise<void> {
|
|
// Step 1: Drop the composite index on monitor_alerts_v2
|
|
try {
|
|
await knex.raw("DROP INDEX IF EXISTS idx_monitor_alerts_v2_config_tag_status");
|
|
} catch (_e) {
|
|
/* index may not exist */
|
|
}
|
|
|
|
// Step 2: Copy first monitor_tag from junction table back to monitor_alerts_config
|
|
const configs = await knex("monitor_alerts_config").select("id");
|
|
|
|
for (const config of configs) {
|
|
const firstMonitor = await knex("monitor_alerts_config_monitors").where({ monitor_alerts_id: config.id }).first();
|
|
|
|
if (firstMonitor) {
|
|
await knex("monitor_alerts_config").where({ id: config.id }).update({ monitor_tag: firstMonitor.monitor_tag });
|
|
}
|
|
}
|
|
|
|
// Step 3: Delete configs that have no monitors (can't satisfy NOT NULL)
|
|
await knex("monitor_alerts_config").whereNull("monitor_tag").del();
|
|
|
|
// Step 4: Re-add foreign key constraint on monitor_alerts_config (non-SQLite only)
|
|
const dbClient = knex.client.config.client;
|
|
if (dbClient !== "sqlite3" && dbClient !== "better-sqlite3") {
|
|
await knex.schema.alterTable("monitor_alerts_config", (table) => {
|
|
table.string("monitor_tag", 255).notNullable().alter();
|
|
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
|
});
|
|
}
|
|
|
|
// Step 5: Drop monitor_tag column from monitor_alerts_v2 (non-SQLite only)
|
|
const hasV2Column = await knex.schema.hasColumn("monitor_alerts_v2", "monitor_tag");
|
|
if (hasV2Column) {
|
|
if (dbClient !== "sqlite3" && dbClient !== "better-sqlite3") {
|
|
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
|
table.dropForeign(["monitor_tag"]);
|
|
table.dropColumn("monitor_tag");
|
|
});
|
|
}
|
|
}
|
|
|
|
// Step 6: Drop junction table
|
|
await knex.schema.dropTableIfExists("monitor_alerts_config_monitors");
|
|
}
|