|
| 1 | +-- SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | +-- Copyright (C) 2026 CrewForm |
| 3 | +-- |
| 4 | +-- 014_marketplace_schema.sql — Marketplace columns on agents + installs & reviews |
| 5 | + |
| 6 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 7 | +-- ALTER agents — add marketplace columns |
| 8 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 9 | + |
| 10 | +ALTER TABLE public.agents |
| 11 | + ADD COLUMN IF NOT EXISTS is_published BOOLEAN NOT NULL DEFAULT false, |
| 12 | + ADD COLUMN IF NOT EXISTS marketplace_tags TEXT[] NOT NULL DEFAULT '{}', |
| 13 | + ADD COLUMN IF NOT EXISTS install_count INTEGER NOT NULL DEFAULT 0, |
| 14 | + ADD COLUMN IF NOT EXISTS rating_avg NUMERIC(2,1) NOT NULL DEFAULT 0, |
| 15 | + ADD COLUMN IF NOT EXISTS provider TEXT; |
| 16 | + |
| 17 | +CREATE INDEX IF NOT EXISTS idx_agents_published |
| 18 | + ON public.agents(is_published) WHERE is_published = true; |
| 19 | + |
| 20 | +CREATE INDEX IF NOT EXISTS idx_agents_marketplace_tags |
| 21 | + ON public.agents USING GIN(marketplace_tags); |
| 22 | + |
| 23 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 24 | +-- agent_installs — tracks who installed which marketplace agent |
| 25 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 26 | + |
| 27 | +CREATE TABLE public.agent_installs ( |
| 28 | + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), |
| 29 | + agent_id UUID NOT NULL REFERENCES public.agents(id) ON DELETE CASCADE, |
| 30 | + workspace_id UUID NOT NULL REFERENCES public.workspaces(id) ON DELETE CASCADE, |
| 31 | + installed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, |
| 32 | + source_workspace_id UUID REFERENCES public.workspaces(id) ON DELETE SET NULL, |
| 33 | + cloned_agent_id UUID REFERENCES public.agents(id) ON DELETE SET NULL, |
| 34 | + installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() |
| 35 | +); |
| 36 | + |
| 37 | +CREATE INDEX idx_agent_installs_agent ON public.agent_installs(agent_id); |
| 38 | +CREATE INDEX idx_agent_installs_workspace ON public.agent_installs(workspace_id); |
| 39 | + |
| 40 | +ALTER TABLE public.agent_installs ENABLE ROW LEVEL SECURITY; |
| 41 | + |
| 42 | +CREATE POLICY "agent_installs_select" ON public.agent_installs |
| 43 | + FOR SELECT USING ( |
| 44 | + workspace_id IN (SELECT workspace_id FROM public.workspace_members WHERE user_id = auth.uid()) |
| 45 | + ); |
| 46 | + |
| 47 | +CREATE POLICY "agent_installs_insert" ON public.agent_installs |
| 48 | + FOR INSERT WITH CHECK ( |
| 49 | + workspace_id IN (SELECT workspace_id FROM public.workspace_members WHERE user_id = auth.uid()) |
| 50 | + ); |
| 51 | + |
| 52 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 53 | +-- agent_reviews — marketplace reviews with 1–5 rating |
| 54 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 55 | + |
| 56 | +CREATE TABLE public.agent_reviews ( |
| 57 | + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), |
| 58 | + agent_id UUID NOT NULL REFERENCES public.agents(id) ON DELETE CASCADE, |
| 59 | + workspace_id UUID NOT NULL REFERENCES public.workspaces(id) ON DELETE CASCADE, |
| 60 | + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, |
| 61 | + rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5), |
| 62 | + review_text TEXT NOT NULL DEFAULT '', |
| 63 | + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), |
| 64 | + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), |
| 65 | + |
| 66 | + -- One review per user per agent |
| 67 | + UNIQUE (agent_id, user_id) |
| 68 | +); |
| 69 | + |
| 70 | +CREATE INDEX idx_agent_reviews_agent ON public.agent_reviews(agent_id); |
| 71 | + |
| 72 | +CREATE TRIGGER trg_agent_reviews_updated_at |
| 73 | + BEFORE UPDATE ON public.agent_reviews |
| 74 | + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at(); |
| 75 | + |
| 76 | +ALTER TABLE public.agent_reviews ENABLE ROW LEVEL SECURITY; |
| 77 | + |
| 78 | +-- Anyone can read published agent reviews |
| 79 | +CREATE POLICY "agent_reviews_select" ON public.agent_reviews |
| 80 | + FOR SELECT USING (true); |
| 81 | + |
| 82 | +CREATE POLICY "agent_reviews_insert" ON public.agent_reviews |
| 83 | + FOR INSERT WITH CHECK (user_id = auth.uid()); |
| 84 | + |
| 85 | +CREATE POLICY "agent_reviews_update" ON public.agent_reviews |
| 86 | + FOR UPDATE USING (user_id = auth.uid()); |
| 87 | + |
| 88 | +CREATE POLICY "agent_reviews_delete" ON public.agent_reviews |
| 89 | + FOR DELETE USING (user_id = auth.uid()); |
| 90 | + |
| 91 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 92 | +-- Trigger: auto-update agents.rating_avg on review changes |
| 93 | +-- ──────────────────────────────────────────────────────────────────────────── |
| 94 | + |
| 95 | +CREATE OR REPLACE FUNCTION public.update_agent_rating() |
| 96 | +RETURNS TRIGGER AS $$ |
| 97 | +DECLARE |
| 98 | + target_agent_id UUID; |
| 99 | + avg_rating NUMERIC(2,1); |
| 100 | +BEGIN |
| 101 | + -- Determine which agent to update |
| 102 | + IF TG_OP = 'DELETE' THEN |
| 103 | + target_agent_id := OLD.agent_id; |
| 104 | + ELSE |
| 105 | + target_agent_id := NEW.agent_id; |
| 106 | + END IF; |
| 107 | + |
| 108 | + -- Calculate new average |
| 109 | + SELECT COALESCE(ROUND(AVG(rating)::NUMERIC, 1), 0) |
| 110 | + INTO avg_rating |
| 111 | + FROM public.agent_reviews |
| 112 | + WHERE agent_id = target_agent_id; |
| 113 | + |
| 114 | + -- Update agent |
| 115 | + UPDATE public.agents |
| 116 | + SET rating_avg = avg_rating |
| 117 | + WHERE id = target_agent_id; |
| 118 | + |
| 119 | + IF TG_OP = 'DELETE' THEN |
| 120 | + RETURN OLD; |
| 121 | + END IF; |
| 122 | + RETURN NEW; |
| 123 | +END; |
| 124 | +$$ LANGUAGE plpgsql SECURITY DEFINER; |
| 125 | + |
| 126 | +CREATE TRIGGER trg_agent_reviews_rating |
| 127 | + AFTER INSERT OR UPDATE OR DELETE ON public.agent_reviews |
| 128 | + FOR EACH ROW EXECUTE FUNCTION public.update_agent_rating(); |
0 commit comments