refactor db
This commit is contained in:
+65
-84
@@ -1,15 +1,20 @@
|
||||
export async function migrate(sql: any) {
|
||||
await sql`CREATE EXTENSION IF NOT EXISTS pgcrypto`;
|
||||
|
||||
// ── accounts ──────────────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
email_hash TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
email_hash TEXT,
|
||||
plan TEXT NOT NULL DEFAULT 'free',
|
||||
plan_expires_at TIMESTAMPTZ,
|
||||
plan_stack JSONB NOT NULL DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
)
|
||||
`;
|
||||
|
||||
// ── monitors ──────────────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS monitors (
|
||||
id TEXT PRIMARY KEY DEFAULT encode(gen_random_bytes(8), 'hex'),
|
||||
@@ -23,25 +28,44 @@ export async function migrate(sql: any) {
|
||||
interval_s INTEGER NOT NULL DEFAULT 60,
|
||||
query JSONB,
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
regions TEXT[] NOT NULL DEFAULT '{}',
|
||||
max_retries INTEGER NOT NULL DEFAULT 0,
|
||||
retry_interval_s INTEGER NOT NULL DEFAULT 30,
|
||||
resend_interval INTEGER NOT NULL DEFAULT 0,
|
||||
cert_alert_days INTEGER NOT NULL DEFAULT 0,
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
channel_ids UUID[] NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
)
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_monitors_account ON monitors(account_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_monitors_tags ON monitors USING GIN(tags)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_monitors_channel_ids ON monitors USING GIN(channel_ids)`;
|
||||
|
||||
// ── pings ─────────────────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS pings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
checked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
scheduled_at TIMESTAMPTZ,
|
||||
jitter_ms INTEGER,
|
||||
status_code INTEGER,
|
||||
latency_ms INTEGER,
|
||||
up BOOLEAN NOT NULL,
|
||||
error TEXT,
|
||||
meta JSONB
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
checked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
scheduled_at TIMESTAMPTZ,
|
||||
jitter_ms INTEGER,
|
||||
status_code INTEGER,
|
||||
latency_ms INTEGER,
|
||||
up BOOLEAN NOT NULL,
|
||||
important BOOLEAN NOT NULL DEFAULT false,
|
||||
error TEXT,
|
||||
meta JSONB,
|
||||
region TEXT NOT NULL DEFAULT 'default',
|
||||
run_id TEXT,
|
||||
cert_expiry_days INTEGER
|
||||
)
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_pings_monitor ON pings(monitor_id, checked_at DESC)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_pings_checked_at ON pings(checked_at)`;
|
||||
|
||||
// ── ping_bodies ───────────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS ping_bodies (
|
||||
ping_id BIGINT PRIMARY KEY REFERENCES pings(id) ON DELETE CASCADE,
|
||||
@@ -49,6 +73,7 @@ export async function migrate(sql: any) {
|
||||
)
|
||||
`;
|
||||
|
||||
// ── api_keys ──────────────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -59,24 +84,9 @@ export async function migrate(sql: any) {
|
||||
last_used_at TIMESTAMPTZ
|
||||
)
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_api_keys_account ON api_keys(account_id)`;
|
||||
|
||||
await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMPTZ`;
|
||||
await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS jitter_ms INTEGER`;
|
||||
await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS regions TEXT[] NOT NULL DEFAULT '{}'`;
|
||||
await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS region TEXT`;
|
||||
await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS run_id TEXT`;
|
||||
await sql`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS plan TEXT NOT NULL DEFAULT 'free'`;
|
||||
await sql`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS plan_expires_at TIMESTAMPTZ`;
|
||||
await sql`ALTER TABLE accounts ADD COLUMN IF NOT EXISTS plan_stack JSONB NOT NULL DEFAULT '[]'`;
|
||||
|
||||
await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS max_retries INTEGER NOT NULL DEFAULT 0`;
|
||||
await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS retry_interval_s INTEGER NOT NULL DEFAULT 30`;
|
||||
await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS resend_interval INTEGER NOT NULL DEFAULT 0`;
|
||||
await sql`ALTER TABLE monitors ADD COLUMN IF NOT EXISTS cert_alert_days INTEGER NOT NULL DEFAULT 0`;
|
||||
await sql`ALTER TABLE monitors ALTER COLUMN cert_alert_days SET DEFAULT 0`;
|
||||
await sql`ALTER TABLE pings ADD COLUMN IF NOT EXISTS important BOOLEAN NOT NULL DEFAULT false`;
|
||||
|
||||
// Per-region transition state. region='' for unspecified/single-region monitors.
|
||||
// ── monitor_region_state ──────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS monitor_region_state (
|
||||
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
@@ -88,11 +98,8 @@ export async function migrate(sql: any) {
|
||||
PRIMARY KEY (monitor_id, region)
|
||||
)
|
||||
`;
|
||||
// Drop the now-stale per-monitor state columns from the previous slice.
|
||||
await sql`ALTER TABLE monitors DROP COLUMN IF EXISTS last_state`;
|
||||
await sql`ALTER TABLE monitors DROP COLUMN IF EXISTS consecutive_down`;
|
||||
|
||||
// Notifications: modular providers, webhook only for now.
|
||||
// ── notification_channels ─────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS notification_channels (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -106,28 +113,7 @@ export async function migrate(sql: any) {
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_notification_channels_account ON notification_channels(account_id)`;
|
||||
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS monitor_notifications (
|
||||
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES notification_channels(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (monitor_id, channel_id)
|
||||
)
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_monitor_notifications_channel ON monitor_notifications(channel_id)`;
|
||||
|
||||
// Tier 3: monitor tags. One row per (monitor, tag). Used by the dashboard
|
||||
// home filter and the status page builder's "all monitors with tag X" picker.
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS monitor_tags (
|
||||
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
tag TEXT NOT NULL,
|
||||
PRIMARY KEY (monitor_id, tag)
|
||||
)
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_monitor_tags_tag ON monitor_tags(tag)`;
|
||||
|
||||
// Tier 3: public status pages. The whole subgraph below is read by the
|
||||
// standalone apps/status service; writes happen via apps/api admin routes.
|
||||
// ── status_pages ──────────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS status_pages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -142,6 +128,9 @@ export async function migrate(sql: any) {
|
||||
show_response_time BOOLEAN NOT NULL DEFAULT true,
|
||||
show_cert_expiry BOOLEAN NOT NULL DEFAULT false,
|
||||
default_window TEXT NOT NULL DEFAULT '24h',
|
||||
display_mode TEXT NOT NULL DEFAULT 'expanded',
|
||||
bar_frequency TEXT NOT NULL DEFAULT 'daily',
|
||||
bar_count INTEGER NOT NULL DEFAULT 90,
|
||||
custom_css TEXT,
|
||||
footer_text TEXT,
|
||||
og_image_url TEXT,
|
||||
@@ -152,14 +141,8 @@ export async function migrate(sql: any) {
|
||||
)
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_status_pages_account ON status_pages(account_id)`;
|
||||
// Display mode: 'compact' = one-line rows with click-to-expand details,
|
||||
// 'expanded' = full detail card always visible (default).
|
||||
await sql`ALTER TABLE status_pages ADD COLUMN IF NOT EXISTS display_mode TEXT NOT NULL DEFAULT 'expanded'`;
|
||||
// Heartbeat bar settings: granularity + how many bars. Independent from
|
||||
// default_window (which still drives the primary uptime % + multi-window cells).
|
||||
await sql`ALTER TABLE status_pages ADD COLUMN IF NOT EXISTS bar_frequency TEXT NOT NULL DEFAULT 'daily'`;
|
||||
await sql`ALTER TABLE status_pages ADD COLUMN IF NOT EXISTS bar_count INTEGER NOT NULL DEFAULT 90`;
|
||||
|
||||
// ── status_page_groups ────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS status_page_groups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -170,20 +153,21 @@ export async function migrate(sql: any) {
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_status_page_groups_page ON status_page_groups(status_page_id)`;
|
||||
|
||||
// ── status_page_monitors ──────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS status_page_monitors (
|
||||
status_page_id UUID NOT NULL REFERENCES status_pages(id) ON DELETE CASCADE,
|
||||
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
group_id UUID REFERENCES status_page_groups(id) ON DELETE SET NULL,
|
||||
display_name TEXT,
|
||||
display_mode TEXT,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (status_page_id, monitor_id)
|
||||
)
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_status_page_monitors_monitor ON status_page_monitors(monitor_id)`;
|
||||
// Per-monitor display mode override. NULL = inherit status_pages.display_mode.
|
||||
await sql`ALTER TABLE status_page_monitors ADD COLUMN IF NOT EXISTS display_mode TEXT`;
|
||||
|
||||
// ── incidents ─────────────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS incidents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -199,6 +183,7 @@ export async function migrate(sql: any) {
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_incidents_account ON incidents(account_id, started_at DESC)`;
|
||||
|
||||
// ── incident_updates ──────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS incident_updates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -211,6 +196,7 @@ export async function migrate(sql: any) {
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_incident_updates_incident ON incident_updates(incident_id, created_at)`;
|
||||
|
||||
// ── incident_monitors ─────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS incident_monitors (
|
||||
incident_id UUID NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,
|
||||
@@ -218,6 +204,9 @@ export async function migrate(sql: any) {
|
||||
PRIMARY KEY (incident_id, monitor_id)
|
||||
)
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_incident_monitors_monitor ON incident_monitors(monitor_id)`;
|
||||
|
||||
// ── incident_status_pages ─────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS incident_status_pages (
|
||||
incident_id UUID NOT NULL REFERENCES incidents(id) ON DELETE CASCADE,
|
||||
@@ -227,8 +216,7 @@ export async function migrate(sql: any) {
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_incident_status_pages_page ON incident_status_pages(status_page_id)`;
|
||||
|
||||
// Shared uptime rollup. One row per (monitor, region, bucket_type, bucket_start).
|
||||
// Powers status page uptime windows AND any future dashboard widgets.
|
||||
// ── monitor_uptime_rollup ─────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS monitor_uptime_rollup (
|
||||
monitor_id TEXT NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
@@ -243,10 +231,7 @@ export async function migrate(sql: any) {
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_uptime_rollup_lookup ON monitor_uptime_rollup(monitor_id, bucket_type, bucket_start DESC)`;
|
||||
|
||||
// Watermark table for the rollup job. Each row records the most recent
|
||||
// pings.checked_at that has been folded into a given bucket_type's rollup
|
||||
// rows. Lets the periodic rollup pass scan only NEW pings since the last
|
||||
// pass instead of re-aggregating the entire current bucket on every tick.
|
||||
// ── rollup_watermarks ─────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS rollup_watermarks (
|
||||
bucket_type TEXT PRIMARY KEY,
|
||||
@@ -254,9 +239,7 @@ export async function migrate(sql: any) {
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_pings_monitor ON pings(monitor_id, checked_at DESC)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_pings_checked_at ON pings(checked_at)`;
|
||||
|
||||
// ── payments ──────────────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
@@ -269,16 +252,19 @@ export async function migrate(sql: any) {
|
||||
address TEXT NOT NULL,
|
||||
derivation_index INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
amount_received TEXT NOT NULL DEFAULT '0',
|
||||
receipt_html TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
paid_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
txid TEXT
|
||||
)
|
||||
`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payments_account ON payments(account_id)`;
|
||||
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_payments_coin_derivation ON payments(coin, derivation_index)`;
|
||||
|
||||
await sql`ALTER TABLE payments ADD COLUMN IF NOT EXISTS amount_received TEXT NOT NULL DEFAULT '0'`;
|
||||
await sql`ALTER TABLE payments ADD COLUMN IF NOT EXISTS receipt_html TEXT`;
|
||||
|
||||
// ── payment_txs ───────────────────────────────────────────────────────
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS payment_txs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
@@ -290,13 +276,8 @@ export async function migrate(sql: any) {
|
||||
UNIQUE(payment_id, txid)
|
||||
)
|
||||
`;
|
||||
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payment_txs_payment ON payment_txs(payment_id)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payment_txs_txid ON payment_txs(txid)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_payments_account ON payments(account_id)`;
|
||||
await sql`DROP INDEX IF EXISTS payments_derivation_index_key`;
|
||||
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_payments_coin_derivation ON payments(coin, derivation_index)`;
|
||||
|
||||
console.log("DB ready");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user