Add 1.18 migrations

This commit is contained in:
Owen
2026-04-21 15:35:19 -07:00
parent e1efae7426
commit 7d9a0cd0cc
5 changed files with 844 additions and 3 deletions

View File

@@ -22,6 +22,7 @@ import m13 from "./scriptsPg/1.15.3";
import m14 from "./scriptsPg/1.15.4";
import m15 from "./scriptsPg/1.16.0";
import m16 from "./scriptsPg/1.17.0";
import m17 from "./scriptsPg/1.18.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -43,7 +44,8 @@ const migrations = [
{ version: "1.15.3", run: m13 },
{ version: "1.15.4", run: m14 },
{ version: "1.16.0", run: m15 },
{ version: "1.17.0", run: m16 }
{ version: "1.17.0", run: m16 },
{ version: "1.18.0", run: m17 }
// Add new migrations here as they are created
] as {
version: string;

View File

@@ -40,6 +40,7 @@ import m34 from "./scriptsSqlite/1.15.3";
import m35 from "./scriptsSqlite/1.15.4";
import m36 from "./scriptsSqlite/1.16.0";
import m37 from "./scriptsSqlite/1.17.0";
import m38 from "./scriptsSqlite/1.18.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -77,7 +78,8 @@ const migrations = [
{ version: "1.15.3", run: m34 },
{ version: "1.15.4", run: m35 },
{ version: "1.16.0", run: m36 },
{ version: "1.17.0", run: m37 }
{ version: "1.17.0", run: m37 },
{ version: "1.18.0", run: m38 }
// Add new migrations here as they are created
] as const;

View File

@@ -0,0 +1,434 @@
import { db } from "@server/db/pg/driver";
import { sql } from "drizzle-orm";
const version = "1.18.0";
export default async function migration() {
console.log(`Running setup script ${version}...`);
// Query existing targetHealthCheck data with joined siteId and orgId before
// the transaction adds the new columns (which start NULL for existing rows).
// We will delete all rows and reinsert them with targetHealthCheckId = targetId
// so the two IDs form a stable 1:1 mapping.
const healthChecksQuery = await db.execute(
sql`SELECT
thc."targetHealthCheckId",
thc."targetId",
t."siteId",
s."orgId",
thc."hcEnabled",
thc."hcPath",
thc."hcScheme",
thc."hcMode",
thc."hcHostname",
thc."hcPort",
thc."hcInterval",
thc."hcUnhealthyInterval",
thc."hcTimeout",
thc."hcHeaders",
thc."hcFollowRedirects",
thc."hcMethod",
thc."hcStatus",
thc."hcHealth",
thc."hcTlsServerName"
FROM "targetHealthCheck" thc
JOIN "targets" t ON thc."targetId" = t."targetId"
JOIN "sites" s ON t."siteId" = s."siteId"`
);
const existingHealthChecks = healthChecksQuery.rows as {
targetHealthCheckId: number;
targetId: number;
siteId: number;
orgId: string;
hcEnabled: boolean;
hcPath: string | null;
hcScheme: string | null;
hcMode: string | null;
hcHostname: string | null;
hcPort: number | null;
hcInterval: number | null;
hcUnhealthyInterval: number | null;
hcTimeout: number | null;
hcHeaders: string | null;
hcFollowRedirects: boolean | null;
hcMethod: string | null;
hcStatus: number | null;
hcHealth: string | null;
hcTlsServerName: string | null;
}[];
console.log(
`Found ${existingHealthChecks.length} existing targetHealthCheck row(s) to migrate`
);
try {
await db.execute(sql`BEGIN`);
await db.execute(sql`
CREATE TABLE "alertEmailActions" (
"emailActionId" serial PRIMARY KEY NOT NULL,
"alertRuleId" integer NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"lastSentAt" bigint
);
`);
await db.execute(sql`
CREATE TABLE "alertEmailRecipients" (
"recipientId" serial PRIMARY KEY NOT NULL,
"emailActionId" integer NOT NULL,
"userId" varchar,
"roleId" integer,
"email" varchar(255)
);
`);
await db.execute(sql`
CREATE TABLE "alertHealthChecks" (
"alertRuleId" integer NOT NULL,
"healthCheckId" integer NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "alertResources" (
"alertRuleId" integer NOT NULL,
"resourceId" integer NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "alertRules" (
"alertRuleId" serial PRIMARY KEY NOT NULL,
"orgId" varchar(255) NOT NULL,
"name" varchar(255) NOT NULL,
"eventType" varchar(100) NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"cooldownSeconds" integer DEFAULT 300 NOT NULL,
"allSites" boolean DEFAULT false NOT NULL,
"allHealthChecks" boolean DEFAULT false NOT NULL,
"allResources" boolean DEFAULT false NOT NULL,
"lastTriggeredAt" bigint,
"createdAt" bigint NOT NULL,
"updatedAt" bigint NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "alertSites" (
"alertRuleId" integer NOT NULL,
"siteId" integer NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "alertWebhookActions" (
"webhookActionId" serial PRIMARY KEY NOT NULL,
"alertRuleId" integer NOT NULL,
"webhookUrl" text NOT NULL,
"config" text,
"enabled" boolean DEFAULT true NOT NULL,
"lastSentAt" bigint
);
`);
await db.execute(sql`
CREATE TABLE "networks" (
"networkId" serial PRIMARY KEY NOT NULL,
"niceId" text,
"name" text,
"scope" varchar DEFAULT 'global' NOT NULL,
"orgId" varchar NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "siteNetworks" (
"siteId" integer NOT NULL,
"networkId" integer NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "statusHistory" (
"id" serial PRIMARY KEY NOT NULL,
"entityType" varchar NOT NULL,
"entityId" integer NOT NULL,
"orgId" varchar NOT NULL,
"status" varchar NOT NULL,
"timestamp" integer NOT NULL
);
`);
await db.execute(sql`
ALTER TABLE "siteResources" DROP CONSTRAINT "siteResources_siteId_sites_siteId_fk";
`);
await db.execute(sql`
ALTER TABLE "targetHealthCheck" ALTER COLUMN "targetId" DROP NOT NULL;
`);
await db.execute(sql`
ALTER TABLE "subscriptions" ADD COLUMN "expiresAt" bigint;
`);
await db.execute(sql`
ALTER TABLE "subscriptions" ADD COLUMN "trial" boolean DEFAULT false;
`);
await db.execute(sql`
ALTER TABLE "requestAuditLog" ADD COLUMN "siteResourceId" integer;
`);
await db.execute(sql`
ALTER TABLE "siteResources" ADD COLUMN "networkId" integer;
`);
await db.execute(sql`
ALTER TABLE "siteResources" ADD COLUMN "defaultNetworkId" integer;
`);
await db.execute(sql`
ALTER TABLE "siteResources" ADD COLUMN "ssl" boolean DEFAULT false NOT NULL;
`);
await db.execute(sql`
ALTER TABLE "siteResources" ADD COLUMN "scheme" varchar;
`);
await db.execute(sql`
ALTER TABLE "siteResources" ADD COLUMN "domainId" varchar;
`);
await db.execute(sql`
ALTER TABLE "siteResources" ADD COLUMN "subdomain" varchar;
`);
await db.execute(sql`
ALTER TABLE "siteResources" ADD COLUMN "fullDomain" varchar;
`);
// Add orgId and siteId as nullable first; NOT NULL constraints are applied
// after the data migration below once every row has been populated.
await db.execute(sql`
ALTER TABLE "targetHealthCheck" ADD COLUMN "orgId" varchar;
`);
await db.execute(sql`
ALTER TABLE "targetHealthCheck" ADD COLUMN "siteId" integer;
`);
await db.execute(sql`
ALTER TABLE "targetHealthCheck" ADD COLUMN "name" varchar;
`);
await db.execute(sql`
ALTER TABLE "targetHealthCheck" ADD COLUMN "hcHealthyThreshold" integer DEFAULT 1;
`);
await db.execute(sql`
ALTER TABLE "targetHealthCheck" ADD COLUMN "hcUnhealthyThreshold" integer DEFAULT 1;
`);
await db.execute(sql`
ALTER TABLE "alertEmailActions" ADD CONSTRAINT "alertEmailActions_alertRuleId_alertRules_alertRuleId_fk" FOREIGN KEY ("alertRuleId") REFERENCES "public"."alertRules"("alertRuleId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertEmailRecipients" ADD CONSTRAINT "alertEmailRecipients_emailActionId_alertEmailActions_emailActionId_fk" FOREIGN KEY ("emailActionId") REFERENCES "public"."alertEmailActions"("emailActionId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertEmailRecipients" ADD CONSTRAINT "alertEmailRecipients_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertEmailRecipients" ADD CONSTRAINT "alertEmailRecipients_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertHealthChecks" ADD CONSTRAINT "alertHealthChecks_alertRuleId_alertRules_alertRuleId_fk" FOREIGN KEY ("alertRuleId") REFERENCES "public"."alertRules"("alertRuleId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertHealthChecks" ADD CONSTRAINT "alertHealthChecks_healthCheckId_targetHealthCheck_targetHealthCheckId_fk" FOREIGN KEY ("healthCheckId") REFERENCES "public"."targetHealthCheck"("targetHealthCheckId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertResources" ADD CONSTRAINT "alertResources_alertRuleId_alertRules_alertRuleId_fk" FOREIGN KEY ("alertRuleId") REFERENCES "public"."alertRules"("alertRuleId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertResources" ADD CONSTRAINT "alertResources_resourceId_resources_resourceId_fk" FOREIGN KEY ("resourceId") REFERENCES "public"."resources"("resourceId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertRules" ADD CONSTRAINT "alertRules_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertSites" ADD CONSTRAINT "alertSites_alertRuleId_alertRules_alertRuleId_fk" FOREIGN KEY ("alertRuleId") REFERENCES "public"."alertRules"("alertRuleId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertSites" ADD CONSTRAINT "alertSites_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "alertWebhookActions" ADD CONSTRAINT "alertWebhookActions_alertRuleId_alertRules_alertRuleId_fk" FOREIGN KEY ("alertRuleId") REFERENCES "public"."alertRules"("alertRuleId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "networks" ADD CONSTRAINT "networks_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "siteNetworks" ADD CONSTRAINT "siteNetworks_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "siteNetworks" ADD CONSTRAINT "siteNetworks_networkId_networks_networkId_fk" FOREIGN KEY ("networkId") REFERENCES "public"."networks"("networkId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "statusHistory" ADD CONSTRAINT "statusHistory_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
CREATE INDEX "idx_statusHistory_entity" ON "statusHistory" USING btree ("entityType","entityId","timestamp");
`);
await db.execute(sql`
CREATE INDEX "idx_statusHistory_org_timestamp" ON "statusHistory" USING btree ("orgId","timestamp");
`);
await db.execute(sql`
ALTER TABLE "siteResources" ADD CONSTRAINT "siteResources_networkId_networks_networkId_fk" FOREIGN KEY ("networkId") REFERENCES "public"."networks"("networkId") ON DELETE set null ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "siteResources" ADD CONSTRAINT "siteResources_defaultNetworkId_networks_networkId_fk" FOREIGN KEY ("defaultNetworkId") REFERENCES "public"."networks"("networkId") ON DELETE restrict ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "siteResources" ADD CONSTRAINT "siteResources_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE set null ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "targetHealthCheck" ADD CONSTRAINT "targetHealthCheck_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "targetHealthCheck" ADD CONSTRAINT "targetHealthCheck_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "siteResources" DROP COLUMN "siteId";
`);
await db.execute(sql`
ALTER TABLE "siteResources" DROP COLUMN "protocol";
`);
await db.execute(sql`COMMIT`);
console.log("Migrated database");
} catch (e) {
await db.execute(sql`ROLLBACK`);
console.log("Unable to migrate database");
console.log(e);
throw e;
}
// Reinsert targetHealthCheck rows with corrected IDs:
// targetHealthCheckId is set to the same integer as targetId (1:1 mapping),
// siteId and orgId are populated from the associated target and site.
//
// Because targetHealthCheckId is a serial (sequence-backed) column, inserting
// explicit values is allowed in PostgreSQL — the sequence is simply bypassed.
// After all inserts we advance the sequence to MAX(targetHealthCheckId) via
// setval() so future auto-inserts never collide with the explicit IDs we used.
if (existingHealthChecks.length > 0) {
try {
// Remove all existing rows first. The alertHealthChecks table is brand
// new in this migration so there are no FK references to worry about.
await db.execute(sql`DELETE FROM "targetHealthCheck"`);
for (const hc of existingHealthChecks) {
await db.execute(sql`
INSERT INTO "targetHealthCheck" (
"targetHealthCheckId",
"targetId",
"orgId",
"siteId",
"hcEnabled",
"hcPath",
"hcScheme",
"hcMode",
"hcHostname",
"hcPort",
"hcInterval",
"hcUnhealthyInterval",
"hcTimeout",
"hcHeaders",
"hcFollowRedirects",
"hcMethod",
"hcStatus",
"hcHealth",
"hcTlsServerName"
) VALUES (
${hc.targetId},
${hc.targetId},
${hc.orgId},
${hc.siteId},
${hc.hcEnabled},
${hc.hcPath},
${hc.hcScheme},
${hc.hcMode},
${hc.hcHostname},
${hc.hcPort},
${hc.hcInterval},
${hc.hcUnhealthyInterval},
${hc.hcTimeout},
${hc.hcHeaders},
${hc.hcFollowRedirects},
${hc.hcMethod},
${hc.hcStatus},
${hc.hcHealth},
${hc.hcTlsServerName}
)
`);
}
// Now that every row has orgId and siteId populated, enforce NOT NULL.
await db.execute(
sql`ALTER TABLE "targetHealthCheck" ALTER COLUMN "orgId" SET NOT NULL`
);
await db.execute(
sql`ALTER TABLE "targetHealthCheck" ALTER COLUMN "siteId" SET NOT NULL`
);
// Advance the sequence so the next auto-insert picks up after the
// largest ID we explicitly wrote. setval(..., max, true) means the
// next nextval() call will return max + 1.
await db.execute(sql`
SELECT setval(
pg_get_serial_sequence('"targetHealthCheck"', 'targetHealthCheckId'),
(SELECT MAX("targetHealthCheckId") FROM "targetHealthCheck"),
true
)
`);
console.log(
`Migrated ${existingHealthChecks.length} targetHealthCheck row(s) with corrected IDs`
);
} catch (e) {
console.error(
"Error while migrating targetHealthCheck rows:",
e
);
throw e;
}
}
console.log(`${version} migration complete`);
}

View File

@@ -0,0 +1,403 @@
import { APP_PATH } from "@server/lib/consts";
import Database from "better-sqlite3";
import path from "path";
const version = "1.18.0";
export default async function migration() {
console.log(`Running setup script ${version}...`);
const location = path.join(APP_PATH, "db", "db.sqlite");
const db = new Database(location);
try {
db.pragma("foreign_keys = OFF");
// Query existing targetHealthCheck data with joined siteId and orgId before
// the transaction drops and recreates the table
const existingHealthChecks = db
.prepare(
`SELECT
thc."targetHealthCheckId",
thc."targetId",
t."siteId",
s."orgId",
thc."hcEnabled",
thc."hcPath",
thc."hcScheme",
thc."hcMode",
thc."hcHostname",
thc."hcPort",
thc."hcInterval",
thc."hcUnhealthyInterval",
thc."hcTimeout",
thc."hcHeaders",
thc."hcFollowRedirects",
thc."hcMethod",
thc."hcStatus",
thc."hcHealth",
thc."hcTlsServerName"
FROM 'targetHealthCheck' thc
JOIN 'targets' t ON thc."targetId" = t."targetId"
JOIN 'sites' s ON t."siteId" = s."siteId"`
)
.all() as {
targetHealthCheckId: number;
targetId: number;
siteId: number;
orgId: string;
hcEnabled: number;
hcPath: string | null;
hcScheme: string | null;
hcMode: string | null;
hcHostname: string | null;
hcPort: number | null;
hcInterval: number | null;
hcUnhealthyInterval: number | null;
hcTimeout: number | null;
hcHeaders: string | null;
hcFollowRedirects: number | null;
hcMethod: string | null;
hcStatus: number | null;
hcHealth: string | null;
hcTlsServerName: string | null;
}[];
console.log(
`Found ${existingHealthChecks.length} existing targetHealthCheck row(s) to migrate`
);
db.transaction(() => {
db.prepare(
`
CREATE TABLE 'alertEmailActions' (
'emailActionId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'alertRuleId' integer NOT NULL,
'enabled' integer DEFAULT true NOT NULL,
'lastSentAt' integer,
FOREIGN KEY ('alertRuleId') REFERENCES 'alertRules'('alertRuleId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'alertEmailRecipients' (
'recipientId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'emailActionId' integer NOT NULL,
'userId' text,
'roleId' integer,
'email' text,
FOREIGN KEY ('emailActionId') REFERENCES 'alertEmailActions'('emailActionId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('roleId') REFERENCES 'roles'('roleId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'alertHealthChecks' (
'alertRuleId' integer NOT NULL,
'healthCheckId' integer NOT NULL,
FOREIGN KEY ('alertRuleId') REFERENCES 'alertRules'('alertRuleId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('healthCheckId') REFERENCES 'targetHealthCheck'('targetHealthCheckId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'alertResources' (
'alertRuleId' integer NOT NULL,
'resourceId' integer NOT NULL,
FOREIGN KEY ('alertRuleId') REFERENCES 'alertRules'('alertRuleId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'alertRules' (
'alertRuleId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'orgId' text NOT NULL,
'name' text NOT NULL,
'eventType' text NOT NULL,
'enabled' integer DEFAULT true NOT NULL,
'cooldownSeconds' integer DEFAULT 300 NOT NULL,
'allSites' integer DEFAULT false NOT NULL,
'allHealthChecks' integer DEFAULT false NOT NULL,
'allResources' integer DEFAULT false NOT NULL,
'lastTriggeredAt' integer,
'createdAt' integer NOT NULL,
'updatedAt' integer NOT NULL,
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'alertSites' (
'alertRuleId' integer NOT NULL,
'siteId' integer NOT NULL,
FOREIGN KEY ('alertRuleId') REFERENCES 'alertRules'('alertRuleId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'alertWebhookActions' (
'webhookActionId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'alertRuleId' integer NOT NULL,
'webhookUrl' text NOT NULL,
'config' text,
'enabled' integer DEFAULT true NOT NULL,
'lastSentAt' integer,
FOREIGN KEY ('alertRuleId') REFERENCES 'alertRules'('alertRuleId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'networks' (
'networkId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'niceId' text,
'name' text,
'scope' text DEFAULT 'global' NOT NULL,
'orgId' text NOT NULL,
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'siteNetworks' (
'siteId' integer NOT NULL,
'networkId' integer NOT NULL,
FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('networkId') REFERENCES 'networks'('networkId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'statusHistory' (
'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'entityType' text NOT NULL,
'entityId' integer NOT NULL,
'orgId' text NOT NULL,
'status' text NOT NULL,
'timestamp' integer NOT NULL,
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE INDEX 'idx_statusHistory_entity' ON 'statusHistory' ('entityType','entityId','timestamp');
`
).run();
db.prepare(
`
CREATE INDEX 'idx_statusHistory_org_timestamp' ON 'statusHistory' ('orgId','timestamp');
`
).run();
db.prepare(
`
CREATE TABLE '__new_siteResources' (
'siteResourceId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'orgId' text NOT NULL,
'networkId' integer,
'defaultNetworkId' integer,
'niceId' text NOT NULL,
'name' text NOT NULL,
'ssl' integer DEFAULT false NOT NULL,
'mode' text NOT NULL,
'scheme' text,
'proxyPort' integer,
'destinationPort' integer,
'destination' text NOT NULL,
'enabled' integer DEFAULT true NOT NULL,
'alias' text,
'aliasAddress' text,
'tcpPortRangeString' text DEFAULT '*' NOT NULL,
'udpPortRangeString' text DEFAULT '*' NOT NULL,
'disableIcmp' integer DEFAULT false NOT NULL,
'authDaemonPort' integer DEFAULT 22123,
'authDaemonMode' text DEFAULT 'site',
'domainId' text,
'subdomain' text,
'fullDomain' text,
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('networkId') REFERENCES 'networks'('networkId') ON UPDATE no action ON DELETE set null,
FOREIGN KEY ('defaultNetworkId') REFERENCES 'networks'('networkId') ON UPDATE no action ON DELETE restrict,
FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE set null
);
`
).run();
db.prepare(
`
INSERT INTO '__new_siteResources'("siteResourceId", "orgId", "networkId", "defaultNetworkId", "niceId", "name", "ssl", "mode", "scheme", "proxyPort", "destinationPort", "destination", "enabled", "alias", "aliasAddress", "tcpPortRangeString", "udpPortRangeString", "disableIcmp", "authDaemonPort", "authDaemonMode", "domainId", "subdomain", "fullDomain") SELECT "siteResourceId", "orgId", "networkId", "defaultNetworkId", "niceId", "name", "ssl", "mode", "scheme", "proxyPort", "destinationPort", "destination", "enabled", "alias", "aliasAddress", "tcpPortRangeString", "udpPortRangeString", "disableIcmp", "authDaemonPort", "authDaemonMode", "domainId", "subdomain", "fullDomain" FROM 'siteResources';
`
).run();
db.prepare(
`
DROP TABLE 'siteResources';
`
).run();
db.prepare(
`
ALTER TABLE '__new_siteResources' RENAME TO 'siteResources';
`
).run();
db.prepare(
`
CREATE TABLE '__new_targetHealthCheck' (
'targetHealthCheckId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'targetId' integer,
'orgId' text NOT NULL,
'siteId' integer NOT NULL,
'name' text,
'hcEnabled' integer DEFAULT false NOT NULL,
'hcPath' text,
'hcScheme' text,
'hcMode' text DEFAULT 'http',
'hcHostname' text,
'hcPort' integer,
'hcInterval' integer DEFAULT 30,
'hcUnhealthyInterval' integer DEFAULT 30,
'hcTimeout' integer DEFAULT 5,
'hcHeaders' text,
'hcFollowRedirects' integer DEFAULT true,
'hcMethod' text DEFAULT 'GET',
'hcStatus' integer,
'hcHealth' text DEFAULT 'unknown',
'hcTlsServerName' text,
'hcHealthyThreshold' integer DEFAULT 1,
'hcUnhealthyThreshold' integer DEFAULT 1,
FOREIGN KEY ('targetId') REFERENCES 'targets'('targetId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade
);
`
).run();
// INSERT INTO '__new_targetHealthCheck'("targetHealthCheckId", "targetId", "orgId", "siteId", "name", "hcEnabled", "hcPath", "hcScheme", "hcMode", "hcHostname", "hcPort", "hcInterval", "hcUnhealthyInterval", "hcTimeout", "hcHeaders", "hcFollowRedirects", "hcMethod", "hcStatus", "hcHealth", "hcTlsServerName", "hcHealthyThreshold", "hcUnhealthyThreshold") SELECT "targetHealthCheckId", "targetId", "orgId", "siteId", "name", "hcEnabled", "hcPath", "hcScheme", "hcMode", "hcHostname", "hcPort", "hcInterval", "hcUnhealthyInterval", "hcTimeout", "hcHeaders", "hcFollowRedirects", "hcMethod", "hcStatus", "hcHealth", "hcTlsServerName", "hcHealthyThreshold", "hcUnhealthyThreshold" FROM 'targetHealthCheck';
db.prepare(
`
DROP TABLE 'targetHealthCheck';
`
).run();
db.prepare(
`
ALTER TABLE '__new_targetHealthCheck' RENAME TO 'targetHealthCheck';
`
).run();
db.prepare(
`
ALTER TABLE 'subscriptions' ADD 'expiresAt' integer;
`
).run();
db.prepare(
`
ALTER TABLE 'subscriptions' ADD 'trial' integer DEFAULT false;
`
).run();
db.prepare(
`
ALTER TABLE 'requestAuditLog' ADD 'siteResourceId' integer;
`
).run();
db.prepare(
`
ALTER TABLE 'sites' ADD 'networkId' integer REFERENCES networks(networkId);
`
).run();
})();
db.pragma("foreign_keys = ON");
// Re-insert targetHealthCheck rows with corrected IDs:
// targetHealthCheckId is set to the same integer as targetId (1:1 mapping),
// siteId and orgId are populated from the associated target and site.
//
// Because targetHealthCheckId is AUTOINCREMENT, inserting explicit values is
// allowed, but sqlite_sequence must be updated afterwards so future
// auto-increments don't reuse or collide with these IDs.
if (existingHealthChecks.length > 0) {
const insertHealthCheck = db.prepare(
`INSERT INTO 'targetHealthCheck' (
"targetHealthCheckId",
"targetId",
"orgId",
"siteId",
"hcEnabled",
"hcPath",
"hcScheme",
"hcMode",
"hcHostname",
"hcPort",
"hcInterval",
"hcUnhealthyInterval",
"hcTimeout",
"hcHeaders",
"hcFollowRedirects",
"hcMethod",
"hcStatus",
"hcHealth",
"hcTlsServerName"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
const insertAll = db.transaction(() => {
for (const hc of existingHealthChecks) {
insertHealthCheck.run(
hc.targetId, // targetHealthCheckId = targetId (explicit, non-sequential is fine)
hc.targetId,
hc.orgId,
hc.siteId,
hc.hcEnabled,
hc.hcPath,
hc.hcScheme,
hc.hcMode,
hc.hcHostname,
hc.hcPort,
hc.hcInterval,
hc.hcUnhealthyInterval,
hc.hcTimeout,
hc.hcHeaders,
hc.hcFollowRedirects,
hc.hcMethod,
hc.hcStatus,
hc.hcHealth,
hc.hcTlsServerName
);
}
});
insertAll();
// Ensure sqlite_sequence reflects the true max so that future
// AUTOINCREMENT inserts never reuse one of the explicitly-set IDs.
// INSERT OR IGNORE handles the case where no auto-insert has happened
// yet and the row doesn't exist in sqlite_sequence.
db.prepare(
`INSERT OR IGNORE INTO sqlite_sequence (name, seq) VALUES ('targetHealthCheck', 0)`
).run();
db.prepare(
`UPDATE sqlite_sequence
SET seq = MAX(seq, (SELECT COALESCE(MAX("targetHealthCheckId"), 0) FROM 'targetHealthCheck'))
WHERE name = 'targetHealthCheck'`
).run();
console.log(
`Migrated ${existingHealthChecks.length} targetHealthCheck row(s) with corrected IDs`
);
}
console.log(`Migrated database`);
} catch (e) {
console.log("Failed to migrate db:", e);
throw e;
}
console.log(`${version} migration complete`);
}