email works now + schema more closely follows original
All checks were successful
Deploy to Server / deploy (push) Successful in 17s
All checks were successful
Deploy to Server / deploy (push) Successful in 17s
This commit is contained in:
175
db/schema.sql
175
db/schema.sql
@@ -1,29 +1,54 @@
|
||||
-- BlindMaster schema. Idempotent: CREATE … IF NOT EXISTS so it works on a
|
||||
-- fresh DB and replays cleanly against an existing one.
|
||||
-- BlindMaster schema. Idempotent: safe on fresh DB and replays cleanly against
|
||||
-- an existing one. Source of truth for table structure, constraints, indexes,
|
||||
-- and triggers. Restored from oldVersion.sql backup + newer additions.
|
||||
|
||||
-- ── Core auth ───────────────────────────────────────────────────────────────
|
||||
-- ── Trigger: auto-delete empty groups ───────────────────────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.delete_group_if_empty() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM group_peripherals WHERE group_id = OLD.group_id) THEN
|
||||
DELETE FROM groups WHERE id = OLD.group_id;
|
||||
END IF;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- ── Core auth ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT,
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash_string TEXT NOT NULL,
|
||||
name TEXT,
|
||||
verification_token TEXT,
|
||||
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
timezone TEXT DEFAULT 'America/Chicago',
|
||||
apns_token TEXT,
|
||||
fcm_token TEXT, -- legacy (FCM era), kept harmless
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- One active session token per user. Code does delete-then-insert on login,
|
||||
-- so PK on user_id enforces it.
|
||||
CREATE TABLE IF NOT EXISTS user_tokens (
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS users_id_idx ON users(id);
|
||||
|
||||
-- UNLOGGED: ephemeral session state — no WAL writes, cleared on crash (correct
|
||||
-- behaviour; clients reconnect anyway). One row per user enforced by PK.
|
||||
CREATE UNLOGGED TABLE IF NOT EXISTS user_tokens (
|
||||
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
connected BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
socket TEXT
|
||||
token TEXT NOT NULL,
|
||||
connected BOOLEAN DEFAULT FALSE,
|
||||
socket VARCHAR(255)
|
||||
);
|
||||
|
||||
-- Convert to UNLOGGED if the table already exists as a logged table.
|
||||
-- user_tokens holds only live socket state; clearing it on restart is correct.
|
||||
DO $$ BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'user_tokens' AND relpersistence = 'p') THEN
|
||||
ALTER TABLE user_tokens SET UNLOGGED;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_tokens_token ON user_tokens(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_tokens_active_socket ON user_tokens(user_id, socket) WHERE connected = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_user_tokens_socket_connected ON user_tokens(socket) WHERE connected = true;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
||||
email TEXT PRIMARY KEY,
|
||||
token TEXT NOT NULL,
|
||||
@@ -38,43 +63,60 @@ CREATE TABLE IF NOT EXISTS user_pending_emails (
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
-- ── Hardware: hubs (devices) and their controlled blinds (peripherals) ──────
|
||||
-- ── Hardware: hubs (devices) and their controlled blinds (peripherals) ───────
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
device_name TEXT NOT NULL,
|
||||
max_ports INTEGER NOT NULL DEFAULT 4,
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
device_name TEXT NOT NULL,
|
||||
max_ports INTEGER NOT NULL DEFAULT 4,
|
||||
battery_soc SMALLINT,
|
||||
timezone TEXT,
|
||||
UNIQUE (user_id, device_name)
|
||||
);
|
||||
|
||||
-- token UNIQUE so socket-auth can `select device_id … where token=$1` quickly.
|
||||
CREATE TABLE IF NOT EXISTS device_tokens (
|
||||
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
connected BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
socket TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS device_tokens_device_id_idx ON device_tokens(device_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_devices_user_id ON devices(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_devices_max_ports ON devices(max_ports);
|
||||
|
||||
-- UNLOGGED: ephemeral device connection state, same rationale as user_tokens.
|
||||
CREATE UNLOGGED TABLE IF NOT EXISTS device_tokens (
|
||||
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL,
|
||||
connected BOOLEAN DEFAULT FALSE,
|
||||
socket VARCHAR(255)
|
||||
);
|
||||
|
||||
DO $$ BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'device_tokens' AND relpersistence = 'p') THEN
|
||||
ALTER TABLE device_tokens SET UNLOGGED;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_device_tokens_token ON device_tokens(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_device_tokens_device_id_connected ON device_tokens(device_id) WHERE connected = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_device_tokens_active_socket ON device_tokens(device_id, socket) WHERE connected = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_device_tokens_socket_connected ON device_tokens(socket) WHERE connected = true;
|
||||
|
||||
-- autovacuum tuned aggressively: last_pos is updated on every blind movement.
|
||||
CREATE TABLE IF NOT EXISTS peripherals (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
peripheral_number INTEGER NOT NULL,
|
||||
peripheral_name TEXT NOT NULL,
|
||||
last_pos INTEGER,
|
||||
last_set TIMESTAMPTZ,
|
||||
calibrated BOOLEAN DEFAULT FALSE,
|
||||
await_calib BOOLEAN DEFAULT FALSE,
|
||||
UNIQUE (device_id, peripheral_number)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS peripherals_user_id_idx ON peripherals(user_id);
|
||||
last_pos INTEGER DEFAULT 0 CHECK (last_pos >= 0 AND last_pos <= 10),
|
||||
last_set TIMESTAMPTZ,
|
||||
UNIQUE (device_id, peripheral_number),
|
||||
UNIQUE (device_id, peripheral_name)
|
||||
) WITH (autovacuum_vacuum_scale_factor = 0.02);
|
||||
|
||||
-- ── Multi-blind groups (e.g. "all kitchen blinds") ──────────────────────────
|
||||
CREATE INDEX IF NOT EXISTS idx_peripherals_device_id ON peripherals(device_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_peripherals_id_user ON peripherals(id, user_id);
|
||||
|
||||
-- ── Multi-blind groups ───────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
timezone TEXT,
|
||||
@@ -82,17 +124,68 @@ CREATE TABLE IF NOT EXISTS groups (
|
||||
UNIQUE (user_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_user_id ON groups(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_peripherals (
|
||||
group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
peripheral_id INTEGER NOT NULL REFERENCES peripherals(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (group_id, peripheral_id)
|
||||
);
|
||||
|
||||
-- ── Idempotent column additions for older DBs ───────────────────────────────
|
||||
-- These mirror the runtime-bootstrap ALTERs the index.js used to do on every
|
||||
-- startup. Safe on fresh DBs because the columns are already in CREATE TABLE.
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS apns_token TEXT;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS timezone TEXT DEFAULT 'America/Chicago';
|
||||
ALTER TABLE devices ADD COLUMN IF NOT EXISTS battery_soc SMALLINT;
|
||||
ALTER TABLE devices ADD COLUMN IF NOT EXISTS timezone TEXT;
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS timezone TEXT;
|
||||
CREATE INDEX IF NOT EXISTS idx_group_peripherals_group_id ON group_peripherals(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_peripherals_peripheral_id ON group_peripherals(peripheral_id);
|
||||
|
||||
-- Trigger fires after any peripheral is removed from a group; deletes the group
|
||||
-- itself if it is now empty.
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_cleanup_empty_groups'
|
||||
) THEN
|
||||
CREATE TRIGGER trigger_cleanup_empty_groups
|
||||
AFTER DELETE ON group_peripherals
|
||||
FOR EACH ROW EXECUTE FUNCTION public.delete_group_if_empty();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ── Schedules ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS schedules (
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
schedule_type VARCHAR(50) NOT NULL CHECK (schedule_type IN ('peripheral', 'group')),
|
||||
peripheral_id INTEGER REFERENCES peripherals(id) ON DELETE CASCADE,
|
||||
device_id INTEGER REFERENCES devices(id) ON DELETE CASCADE,
|
||||
peripheral_number INTEGER,
|
||||
group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
|
||||
target_position INTEGER NOT NULL,
|
||||
cron_expression VARCHAR(100) NOT NULL,
|
||||
cron_minute VARCHAR(50) NOT NULL,
|
||||
cron_hour VARCHAR(50) NOT NULL,
|
||||
cron_days VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT peripheral_or_group CHECK (
|
||||
(schedule_type = 'peripheral' AND peripheral_id IS NOT NULL AND group_id IS NULL) OR
|
||||
(schedule_type = 'group' AND group_id IS NOT NULL AND peripheral_id IS NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_schedules_user_id ON schedules(user_id);
|
||||
|
||||
-- Prevent duplicate cron times for the same target.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_peripheral_schedule
|
||||
ON schedules(peripheral_id, cron_expression)
|
||||
WHERE schedule_type = 'peripheral';
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_group_schedule
|
||||
ON schedules(group_id, cron_expression)
|
||||
WHERE schedule_type = 'group';
|
||||
|
||||
-- ── Idempotent column additions for older DBs ────────────────────────────────
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS apns_token TEXT;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS timezone TEXT DEFAULT 'America/Chicago';
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS verification_token TEXT;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_verified BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
||||
ALTER TABLE devices ADD COLUMN IF NOT EXISTS battery_soc SMALLINT;
|
||||
ALTER TABLE devices ADD COLUMN IF NOT EXISTS timezone TEXT;
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS timezone TEXT;
|
||||
|
||||
Reference in New Issue
Block a user