Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
150 lines
6.7 KiB
SQL
150 lines
6.7 KiB
SQL
-- ==========================================
|
|
-- Chat Switchboard — 018 Workflows
|
|
-- ==========================================
|
|
-- Workflow definitions, stages, version snapshots, assignments.
|
|
-- Consolidated v0.28.0: merges 023 + 025 (assignments) + 027 (webhooks).
|
|
-- Also adds deferred FK: channels.workflow_id → workflows.id.
|
|
-- ==========================================
|
|
|
|
-- =========================================
|
|
-- WORKFLOW DEFINITIONS
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS workflows (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
slug TEXT NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
branding JSONB NOT NULL DEFAULT '{}',
|
|
entry_mode TEXT NOT NULL DEFAULT 'public_link'
|
|
CHECK (entry_mode IN ('public_link', 'team_only')),
|
|
is_active BOOLEAN NOT NULL DEFAULT false,
|
|
version INTEGER NOT NULL DEFAULT 1,
|
|
on_complete JSONB,
|
|
retention JSONB NOT NULL DEFAULT '{"mode": "archive"}',
|
|
|
|
-- Webhooks (v0.27.3)
|
|
webhook_url TEXT,
|
|
webhook_secret TEXT,
|
|
|
|
created_by UUID NOT NULL REFERENCES users(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug
|
|
ON workflows(team_id, slug) WHERE team_id IS NOT NULL;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug
|
|
ON workflows(slug) WHERE team_id IS NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_workflows_active
|
|
ON workflows(is_active) WHERE is_active = true;
|
|
|
|
DROP TRIGGER IF EXISTS workflows_updated_at ON workflows;
|
|
CREATE TRIGGER workflows_updated_at
|
|
BEFORE UPDATE ON workflows
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
COMMENT ON TABLE workflows IS 'Team-owned or global workflow definitions with staged processes.';
|
|
COMMENT ON COLUMN workflows.slug IS 'URL-safe identifier, unique within scope (team or global).';
|
|
COMMENT ON COLUMN workflows.branding IS '{"accent_color": "#hex", "logo_url": "...", "tagline": "..."}';
|
|
COMMENT ON COLUMN workflows.on_complete IS 'Chaining hook — triggers another workflow on completion.';
|
|
COMMENT ON COLUMN workflows.retention IS '{"mode": "archive"|"delete", "delete_after_days": N}';
|
|
|
|
|
|
-- =========================================
|
|
-- WORKFLOW STAGES
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS workflow_stages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
ordinal INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
persona_id UUID REFERENCES personas(id) ON DELETE SET NULL,
|
|
assignment_team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
|
|
form_template JSONB NOT NULL DEFAULT '{}',
|
|
stage_mode TEXT NOT NULL DEFAULT 'chat_only'
|
|
CHECK (stage_mode IN ('chat_only', 'form_only', 'form_chat', 'review')),
|
|
history_mode TEXT NOT NULL DEFAULT 'full'
|
|
CHECK (history_mode IN ('full', 'summary', 'fresh')),
|
|
auto_transition BOOLEAN NOT NULL DEFAULT false,
|
|
transition_rules JSONB NOT NULL DEFAULT '{}',
|
|
surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL,
|
|
sla_seconds INTEGER,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workflow_stages_workflow
|
|
ON workflow_stages(workflow_id, ordinal);
|
|
|
|
COMMENT ON TABLE workflow_stages IS 'Ordered stages within a workflow. Each stage has a driving persona and optional human assignment.';
|
|
COMMENT ON COLUMN workflow_stages.history_mode IS 'full=complete history, summary=utility-role summary, fresh=clean slate';
|
|
COMMENT ON COLUMN workflow_stages.form_template IS 'JSON schema of fields the persona should collect from the visitor.';
|
|
COMMENT ON COLUMN workflow_stages.surface_pkg_id IS 'Optional package that provides a custom stage surface. NULL = built-in surface based on stage_mode.';
|
|
COMMENT ON COLUMN workflow_stages.sla_seconds IS 'SLA budget in seconds for this stage. NULL = no SLA.';
|
|
|
|
|
|
-- =========================================
|
|
-- WORKFLOW VERSIONS (immutable snapshots)
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS workflow_versions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
version_number INTEGER NOT NULL,
|
|
snapshot JSONB NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
UNIQUE (workflow_id, version_number)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow
|
|
ON workflow_versions(workflow_id, version_number DESC);
|
|
|
|
COMMENT ON TABLE workflow_versions IS 'Immutable snapshots of workflow definitions. Active instances run against a pinned version.';
|
|
COMMENT ON COLUMN workflow_versions.snapshot IS 'Full JSON: definition + stages + persona tool grants at publish time.';
|
|
|
|
|
|
-- =========================================
|
|
-- WORKFLOW ASSIGNMENTS
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS workflow_assignments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
stage INTEGER NOT NULL,
|
|
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
|
assigned_to UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
status TEXT NOT NULL DEFAULT 'unassigned'
|
|
CHECK (status IN ('unassigned', 'claimed', 'completed')),
|
|
review_comments JSONB NOT NULL DEFAULT '[]',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
claimed_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_channel
|
|
ON workflow_assignments(channel_id, stage);
|
|
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_team_status
|
|
ON workflow_assignments(team_id, status) WHERE status = 'unassigned';
|
|
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_user
|
|
ON workflow_assignments(assigned_to) WHERE assigned_to IS NOT NULL;
|
|
|
|
COMMENT ON TABLE workflow_assignments IS 'Queue entries for human review stages in workflows.';
|
|
COMMENT ON COLUMN workflow_assignments.status IS 'unassigned=waiting, claimed=being worked, completed=done';
|
|
|
|
|
|
-- =========================================
|
|
-- DEFERRED FK: channels.workflow_id → workflows
|
|
-- =========================================
|
|
|
|
DO $$ BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_workflow'
|
|
) THEN
|
|
ALTER TABLE channels ADD CONSTRAINT fk_channels_workflow
|
|
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE SET NULL;
|
|
END IF;
|
|
END $$;
|