Files
MarketingTool/apps/api/schema.sql
T

117 lines
8.1 KiB
SQL

PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS organizations (
id TEXT PRIMARY KEY, name TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id), email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, password_salt TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('viewer','owner','admin','researcher')),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_org ON users(organization_id);
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token_hash);
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT REFERENCES organizations(id), user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
action TEXT NOT NULL, details TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_audit_org ON audit_log(organization_id,created_at);
CREATE TABLE IF NOT EXISTS businesses (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL, name TEXT NOT NULL, website TEXT NOT NULL DEFAULT '', website_domain TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '', phone TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', score INTEGER NOT NULL DEFAULT 0,
score_version TEXT NOT NULL DEFAULT 'mvp-1', score_factors TEXT NOT NULL DEFAULT '[]', website_class TEXT NOT NULL DEFAULT 'missing',
verified INTEGER NOT NULL DEFAULT 0, verified_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_businesses_org ON businesses(organization_id);
CREATE INDEX IF NOT EXISTS idx_businesses_score ON businesses(organization_id,score DESC,id);
CREATE INDEX IF NOT EXISTS idx_businesses_class ON businesses(organization_id,website_class);
CREATE UNIQUE INDEX IF NOT EXISTS uq_business_domain ON businesses(organization_id, website_domain) WHERE website_domain <> '';
CREATE UNIQUE INDEX IF NOT EXISTS uq_business_email ON businesses(organization_id, email) WHERE email <> '';
CREATE UNIQUE INDEX IF NOT EXISTS uq_business_phone ON businesses(organization_id, phone) WHERE phone <> '';
CREATE TABLE IF NOT EXISTS suppressions (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('email','domain','phone')),
value TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(organization_id, kind, value)
);
CREATE TABLE IF NOT EXISTS business_identifiers (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), kind TEXT NOT NULL, value TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), name TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '', phone TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL DEFAULT '', do_not_contact INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS domains (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), domain TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'other', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS websites (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), url TEXT NOT NULL, website_class TEXT NOT NULL DEFAULT 'business_site', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS evidence (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), kind TEXT NOT NULL, url TEXT NOT NULL DEFAULT '', claim TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS pipeline_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), stage TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS interactions (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), kind TEXT NOT NULL, body TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), body TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_contacts_business ON contacts(business_id);
CREATE INDEX IF NOT EXISTS idx_contacts_org_business ON contacts(organization_id,business_id);
CREATE INDEX IF NOT EXISTS idx_identifiers_business ON business_identifiers(business_id,kind);
CREATE INDEX IF NOT EXISTS idx_domains_business ON domains(business_id);
CREATE INDEX IF NOT EXISTS idx_websites_business ON websites(business_id);
CREATE INDEX IF NOT EXISTS idx_evidence_business ON evidence(business_id);
CREATE INDEX IF NOT EXISTS idx_pipeline_business ON pipeline_entries(business_id,created_at);
CREATE INDEX IF NOT EXISTS idx_pipeline_stage ON pipeline_entries(organization_id,stage);
CREATE INDEX IF NOT EXISTS idx_notes_business ON notes(business_id,created_at);
CREATE INDEX IF NOT EXISTS idx_interactions_business ON interactions(business_id,created_at);
-- Phase 4 durable background jobs (additive-safe for existing databases).
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
organization_id TEXT NOT NULL REFERENCES organizations(id),
idempotency_key TEXT NOT NULL,
type TEXT NOT NULL,
payload TEXT NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'queued' CHECK(status IN ('queued','running','succeeded','failed','cancelled')),
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 3,
progress INTEGER NOT NULL DEFAULT 0 CHECK(progress BETWEEN 0 AND 100),
error_code TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at TEXT,
completed_at TEXT,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(organization_id,idempotency_key)
);
CREATE INDEX IF NOT EXISTS idx_jobs_org_created ON jobs(organization_id,created_at DESC,id DESC);
CREATE INDEX IF NOT EXISTS idx_jobs_queue ON jobs(status,created_at,id);
CREATE TABLE IF NOT EXISTS job_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id),
sequence INTEGER NOT NULL,
event_type TEXT NOT NULL,
message TEXT NOT NULL DEFAULT '',
progress INTEGER NOT NULL DEFAULT 0 CHECK(progress BETWEEN 0 AND 100),
error_code TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(job_id,sequence)
);
CREATE INDEX IF NOT EXISTS idx_job_events_job_sequence ON job_events(job_id,sequence);