wacrm/supabase/migrations/006_automations.sql
Arnas Donauskas b7b362ae42
feat(automations): visual flow builder, engine, and logs viewer
Replaces the /automations placeholder with a full feature:
- Migration 006 adds automations, automation_steps, automation_logs,
  and automation_pending_executions with RLS matching existing tables.
- List page with active/inactive switch, quick-start templates,
  duplicate, delete, and View Logs actions.
- Full-screen vertical flow builder with Trigger + Action + Condition
  (Yes/No branches) + Wait cards. Inline edit forms, up/down reorder,
  dotted-grid canvas.
- Execution engine (src/lib/automations/engine.ts) dispatched from the
  WhatsApp webhook for new_message_received, new_contact_created, and
  keyword_match. Wait steps enqueue to automation_pending_executions;
  /api/automations/cron drains them behind AUTOMATION_CRON_SECRET.
- Logs viewer with per-step checkmarks and expandable detail.

Existing files touched only in webhook/route.ts (fire-and-forget
trigger dispatch) and types/index.ts (appended automation types).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 19:39:02 +03:00

141 lines
6.2 KiB
SQL

-- ============================================================
-- 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.