From 1709a331d1954656cf2ca92f96df34ccf44d47ce Mon Sep 17 00:00:00 2001 From: sravan Date: Fri, 24 Jul 2026 19:46:04 +0530 Subject: [PATCH] feat(db): Postgres target schema (Phase 1 of the migration) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit server/db/schema.pg.sql — the full Postgres DDL, every column defined up front (no ALTER-ordering fragility). SQLite→PG type mapping documented in-file (epoch-ms INTEGER→BIGINT, 0/1 flags→SMALLINT kept numeric so app code is unchanged, sizes→ BIGINT, audit rowid→GENERATED IDENTITY). Mirrors the three existing FKs and adds a new idx_messages_attachment (the /files auth scan we cached earlier becomes a keyed lookup). Validated against a throwaway Postgres 16: loads with no errors, 24 tables + 44 indexes created. Unwired — nothing uses it yet; the SQLite path is untouched. Co-Authored-By: Claude Opus 4.8 --- server/db/schema.pg.sql | 304 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 server/db/schema.pg.sql diff --git a/server/db/schema.pg.sql b/server/db/schema.pg.sql new file mode 100644 index 0000000..203ba8f --- /dev/null +++ b/server/db/schema.pg.sql @@ -0,0 +1,304 @@ +-- PostgreSQL schema for Biz Connect — the target of the SQLite→Postgres migration. +-- +-- Every column is defined up front here (unlike the SQLite db.js, which layers columns via ALTER TABLE +-- and hit a real ordering bug). Type mapping from the SQLite schema: +-- SQLite INTEGER epoch-ms timestamp -> BIGINT (ms since epoch; JS Number-safe) +-- SQLite INTEGER 0/1 boolean flag -> SMALLINT (kept numeric so app code still reads 0/1) +-- SQLite INTEGER byte size / duration-> BIGINT (files can exceed INT range) +-- SQLite INTEGER small count (peak) -> INTEGER +-- SQLite AUTOINCREMENT rowid -> BIGINT GENERATED ALWAYS AS IDENTITY +-- TEXT -> TEXT +-- Foreign keys mirror the three the SQLite schema enforced (users→teams, sessions_auth→users, +-- machines→teams). The data-migration script inserts in dependency order so these hold. + +CREATE TABLE IF NOT EXISTS teams ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL REFERENCES teams(id), + email TEXT NOT NULL UNIQUE, + pw_hash TEXT NOT NULL, + pw_salt TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'technician', + mfa_secret TEXT, + mfa_enabled SMALLINT NOT NULL DEFAULT 0, + created_at BIGINT NOT NULL, + name TEXT, + active SMALLINT NOT NULL DEFAULT 1, + avatar_url TEXT, + status TEXT NOT NULL DEFAULT 'active', + bizgaze_user_id TEXT, + last_seen BIGINT +); +CREATE INDEX IF NOT EXISTS idx_users_bizgaze_user_id ON users(bizgaze_user_id); + +CREATE TABLE IF NOT EXISTS sessions_auth ( + token TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES users(id), + mfa_passed SMALLINT NOT NULL DEFAULT 0, + created_at BIGINT NOT NULL, + expires_at BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS machines ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL REFERENCES teams(id), + name TEXT NOT NULL, + enroll_token TEXT NOT NULL UNIQUE, + unattended SMALLINT NOT NULL DEFAULT 0, + last_seen BIGINT, + created_at BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS audit_log ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + team_id TEXT NOT NULL, + user_id TEXT, + user_email TEXT, + machine_id TEXT, + machine_name TEXT, + action TEXT NOT NULL, + detail TEXT, + at BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS sessions_log ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + agent_email TEXT, + agent_name TEXT, + ticket TEXT, + started_at BIGINT NOT NULL, + ended_at BIGINT, + recording TEXT, + transcript TEXT +); + +CREATE TABLE IF NOT EXISTS refresh_tokens ( + token_hash TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + created_at BIGINT NOT NULL, + expires_at BIGINT NOT NULL, + revoked SMALLINT NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS api_keys ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + name TEXT, + key_hash TEXT NOT NULL UNIQUE, + scopes TEXT NOT NULL DEFAULT '', + created_by TEXT, + created_at BIGINT NOT NULL, + last_used_at BIGINT, + revoked SMALLINT NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS webhooks ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + url TEXT NOT NULL, + secret TEXT NOT NULL, + events TEXT NOT NULL DEFAULT '', + active SMALLINT NOT NULL DEFAULT 1, + created_by TEXT, + created_at BIGINT NOT NULL, + last_status INTEGER, + last_error TEXT, + last_at BIGINT +); + +CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + sender_id TEXT NOT NULL, + recipient_id TEXT NOT NULL, + body TEXT NOT NULL, + created_at BIGINT NOT NULL, + read_at BIGINT, + reply_to TEXT, + attachment_id TEXT, + conversation_id TEXT, + mentions TEXT, + delivered_at BIGINT, + poll_id TEXT, + msg_type TEXT, + deleted SMALLINT NOT NULL DEFAULT 0, + edited_at BIGINT, + fwd_from TEXT +); +CREATE INDEX IF NOT EXISTS idx_messages_pair ON messages(team_id, sender_id, recipient_id, created_at); +CREATE INDEX IF NOT EXISTS idx_messages_unread ON messages(team_id, recipient_id, sender_id, read_at); +CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id, created_at); +-- New: attachment lookups drove the /files auth scan (see static.js authAttachment). Index it so the +-- per-Range playback auth is a keyed lookup, not a table scan (the auth cache stays as a second line). +CREATE INDEX IF NOT EXISTS idx_messages_attachment ON messages(attachment_id); + +CREATE TABLE IF NOT EXISTS message_reactions ( + message_id TEXT NOT NULL, + user_id TEXT NOT NULL, + emoji TEXT NOT NULL, + created_at BIGINT NOT NULL, + PRIMARY KEY (message_id, user_id, emoji) +); + +CREATE TABLE IF NOT EXISTS attachments ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + uploader_id TEXT NOT NULL, + name TEXT NOT NULL, + mime TEXT, + size BIGINT, + created_at BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS conversations ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + type TEXT NOT NULL DEFAULT 'group', + name TEXT, + created_by TEXT, + created_at BIGINT NOT NULL, + avatar_id TEXT, + admin_only SMALLINT NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS conversation_members ( + conversation_id TEXT NOT NULL, + user_id TEXT NOT NULL, + last_read_at BIGINT NOT NULL DEFAULT 0, + joined_at BIGINT NOT NULL, + admin SMALLINT NOT NULL DEFAULT 0, + PRIMARY KEY (conversation_id, user_id) +); + +CREATE TABLE IF NOT EXISTS call_history ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + room TEXT NOT NULL, + group_id TEXT, + kind TEXT, + title TEXT, + peak INTEGER NOT NULL DEFAULT 0, + participants TEXT, + uids TEXT, + started_at BIGINT NOT NULL, + ended_at BIGINT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_call_history_team ON call_history(team_id, ended_at); + +CREATE TABLE IF NOT EXISTS user_aliases ( + old_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + team_id TEXT, + created_at BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS polls ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + conversation_id TEXT NOT NULL, + message_id TEXT, + question TEXT NOT NULL, + options TEXT NOT NULL, + multi SMALLINT NOT NULL DEFAULT 0, + closed SMALLINT NOT NULL DEFAULT 0, + created_by TEXT NOT NULL, + created_at BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS poll_votes ( + poll_id TEXT NOT NULL, + user_id TEXT NOT NULL, + option_idx INTEGER NOT NULL, + created_at BIGINT NOT NULL, + PRIMARY KEY (poll_id, user_id, option_idx) +); + +CREATE TABLE IF NOT EXISTS scheduled_meetings ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + group_id TEXT, + room_code TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + scheduled_at BIGINT NOT NULL, + created_by TEXT NOT NULL, + created_at BIGINT NOT NULL, + ended_at BIGINT, + participants TEXT, + reminded SMALLINT NOT NULL DEFAULT 0, + cancelled SMALLINT NOT NULL DEFAULT 0, + duration_mins INTEGER, + recurrence TEXT, + guest_emails TEXT, + lobby SMALLINT +); +CREATE INDEX IF NOT EXISTS idx_sched_team ON scheduled_meetings(team_id, scheduled_at); +CREATE INDEX IF NOT EXISTS idx_sched_code ON scheduled_meetings(room_code); + +CREATE TABLE IF NOT EXISTS recordings ( + id TEXT PRIMARY KEY, + team_id TEXT NOT NULL, + room TEXT, + group_id TEXT, + meeting_id TEXT, + title TEXT, + kind TEXT NOT NULL, + file TEXT, + mime TEXT, + size BIGINT, + duration_ms BIGINT, + created_by TEXT, + created_by_name TEXT, + created_at BIGINT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_rec_team ON recordings(team_id, created_at); +CREATE INDEX IF NOT EXISTS idx_rec_room ON recordings(room); + +CREATE TABLE IF NOT EXISTS push_subscriptions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + endpoint TEXT NOT NULL UNIQUE, + p256dh TEXT NOT NULL, + auth TEXT NOT NULL, + created_at BIGINT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_push_user ON push_subscriptions(user_id); + +CREATE TABLE IF NOT EXISTS device_tokens ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + tenant_id TEXT, + platform TEXT NOT NULL, + token TEXT NOT NULL UNIQUE, + created_at BIGINT NOT NULL, + last_seen BIGINT +); +CREATE INDEX IF NOT EXISTS idx_devtok_user ON device_tokens(user_id); + +CREATE TABLE IF NOT EXISTS app_installs ( + id TEXT PRIMARY KEY, + install_id TEXT NOT NULL UNIQUE, + user_id TEXT, + user_email TEXT, + tenant_id TEXT, + platform TEXT, + app_version TEXT, + os TEXT, + first_seen BIGINT NOT NULL, + last_seen BIGINT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_installs_tenant ON app_installs(tenant_id); +CREATE INDEX IF NOT EXISTS idx_installs_user ON app_installs(user_id); + +CREATE TABLE IF NOT EXISTS favorites ( + user_id TEXT NOT NULL, + target TEXT NOT NULL, + created_at BIGINT NOT NULL, + PRIMARY KEY (user_id, target) +);