feat(db): Postgres target schema (Phase 1 of the migration)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
);
|
||||
Reference in New Issue
Block a user