-- ============================================================ -- 006_automations.sql — Automations feature -- -- Idempotent migration — safe to run multiple times. -- Follows the same conventions as 001_initial_schema.sql: -- IF NOT EXISTS on tables/indexes, DROP IF EXISTS before -- re-creating policies/triggers (Postgres has no -- CREATE POLICY IF NOT EXISTS). -- ============================================================ -- ============================================================ -- AUTOMATIONS -- ============================================================ CREATE TABLE IF NOT EXISTS automations ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, trigger_type TEXT NOT NULL, trigger_config JSONB NOT NULL DEFAULT '{}'::jsonb, is_active BOOLEAN NOT NULL DEFAULT FALSE, execution_count INTEGER NOT NULL DEFAULT 0, last_executed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_automations_user_id ON automations(user_id); -- Partial index tuned for the engine's hot path: find active automations -- whose trigger_type matches the fired event. RLS then narrows by user_id. CREATE INDEX IF NOT EXISTS idx_automations_active_trigger ON automations(trigger_type) WHERE is_active = TRUE; ALTER TABLE automations ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage own automations" ON automations; CREATE POLICY "Users can manage own automations" ON automations FOR ALL USING (auth.uid() = user_id); DROP TRIGGER IF EXISTS set_updated_at ON automations; CREATE TRIGGER set_updated_at BEFORE UPDATE ON automations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ============================================================ -- AUTOMATION_STEPS -- -- `position` — order within parent scope (root scope or a branch). -- `parent_step_id` — NULL for root-level steps; set to the Condition -- step's id for steps that live inside one of its -- branches. -- `branch` — NULL for root steps. For children of a Condition, -- 'yes' or 'no' identifying which path. -- ============================================================ CREATE TABLE IF NOT EXISTS automation_steps ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), automation_id UUID NOT NULL REFERENCES automations(id) ON DELETE CASCADE, parent_step_id UUID REFERENCES automation_steps(id) ON DELETE CASCADE, branch TEXT CHECK (branch IN ('yes', 'no')), step_type TEXT NOT NULL, step_config JSONB NOT NULL DEFAULT '{}'::jsonb, position INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_automation_steps_automation_id ON automation_steps(automation_id, position); CREATE INDEX IF NOT EXISTS idx_automation_steps_parent ON automation_steps(parent_step_id) WHERE parent_step_id IS NOT NULL; ALTER TABLE automation_steps ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can manage steps of own automations" ON automation_steps; CREATE POLICY "Users can manage steps of own automations" ON automation_steps FOR ALL USING ( EXISTS ( SELECT 1 FROM automations a WHERE a.id = automation_steps.automation_id AND a.user_id = auth.uid() ) ); -- ============================================================ -- AUTOMATION_LOGS -- -- user_id is denormalized for simple RLS; contact_id is nullable so -- history survives contact deletion (mirrors migration 004's pattern -- on broadcast_recipients / deals). -- ============================================================ CREATE TABLE IF NOT EXISTS automation_logs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), automation_id UUID NOT NULL REFERENCES automations(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL, trigger_event TEXT NOT NULL, steps_executed JSONB NOT NULL DEFAULT '[]'::jsonb, status TEXT NOT NULL CHECK (status IN ('success', 'partial', 'failed')), error_message TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_automation_logs_automation ON automation_logs(automation_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_automation_logs_user ON automation_logs(user_id); ALTER TABLE automation_logs ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Users can view own automation logs" ON automation_logs; CREATE POLICY "Users can view own automation logs" ON automation_logs FOR ALL USING (auth.uid() = user_id); -- ============================================================ -- AUTOMATION_PENDING_EXECUTIONS -- -- Queue row created when a running automation hits a `wait` step. -- The cron endpoint drains rows where run_at <= now() and status = -- 'pending', flips them to 'running', and resumes the automation -- from `next_step_position` with the saved `context` jsonb. -- -- Service-role only — writes never originate from the browser, and -- the engine uses the service-role client. No user policy exposed. -- ============================================================ CREATE TABLE IF NOT EXISTS automation_pending_executions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), automation_id UUID NOT NULL REFERENCES automations(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL, log_id UUID REFERENCES automation_logs(id) ON DELETE CASCADE, parent_step_id UUID REFERENCES automation_steps(id) ON DELETE SET NULL, branch TEXT CHECK (branch IN ('yes', 'no')), next_step_position INTEGER NOT NULL, context JSONB NOT NULL DEFAULT '{}'::jsonb, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'done', 'failed')), run_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_automation_pending_due ON automation_pending_executions(run_at) WHERE status = 'pending'; ALTER TABLE automation_pending_executions ENABLE ROW LEVEL SECURITY; -- No SELECT/INSERT/UPDATE/DELETE policy for authenticated users — all -- access is server-side via the service-role key.