mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 06:56:44 +00:00
252 lines
No EOL
9 KiB
PL/PgSQL
252 lines
No EOL
9 KiB
PL/PgSQL
-- ============================================================
|
|
-- FASE 2: Skill Studio — Schema completo
|
|
-- ============================================================
|
|
|
|
-- ------------------------------------------------------------
|
|
-- 1) agent_instances: garantir unicidade por user_id e popular retroativamente
|
|
-- ------------------------------------------------------------
|
|
|
|
-- Garante que cada usuário tem no máximo 1 agente (necessário para ON CONFLICT no webhook)
|
|
CREATE UNIQUE INDEX IF NOT EXISTS agent_instances_user_id_key
|
|
ON public.agent_instances(user_id);
|
|
|
|
-- Migração retroativa: cria agent_instance 'active' para quem já tem subscription ativa/trial
|
|
INSERT INTO public.agent_instances (user_id, status, created_at, updated_at)
|
|
SELECT
|
|
s.user_id,
|
|
'active' AS status,
|
|
now(),
|
|
now()
|
|
FROM public.subscriptions s
|
|
WHERE s.status IN ('active', 'trialing')
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM public.agent_instances ai WHERE ai.user_id = s.user_id
|
|
);
|
|
|
|
-- ------------------------------------------------------------
|
|
-- 2) Tabela: skills
|
|
-- ------------------------------------------------------------
|
|
CREATE TABLE public.skills (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id uuid NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
|
|
agent_instance_id uuid NOT NULL REFERENCES public.agent_instances(id) ON DELETE CASCADE,
|
|
name text NOT NULL,
|
|
description text NOT NULL,
|
|
trigger_keywords text NOT NULL,
|
|
status text NOT NULL DEFAULT 'draft'
|
|
CHECK (status IN ('draft', 'testing', 'active', 'disabled', 'archived')),
|
|
current_version_id uuid, -- SEM FK por design (ver comment)
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
COMMENT ON COLUMN public.skills.current_version_id IS
|
|
'Aponta para skill_versions(id). Sem FK constraint para evitar dependência circular com skill_versions (que referencia skills.id). A integridade é garantida pela aplicação e pela edge function publish-skill-version.';
|
|
|
|
CREATE INDEX idx_skills_user_id ON public.skills(user_id);
|
|
CREATE INDEX idx_skills_agent_instance_id ON public.skills(agent_instance_id);
|
|
CREATE INDEX idx_skills_status ON public.skills(status);
|
|
|
|
-- Invariante: nome único por usuário entre não-arquivadas
|
|
CREATE UNIQUE INDEX skills_unique_name_per_user
|
|
ON public.skills(user_id, name)
|
|
WHERE status != 'archived';
|
|
|
|
ALTER TABLE public.skills ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "Usuários veem suas próprias skills"
|
|
ON public.skills FOR SELECT
|
|
USING (auth.uid() = user_id);
|
|
|
|
CREATE POLICY "Usuários criam suas próprias skills"
|
|
ON public.skills FOR INSERT
|
|
WITH CHECK (auth.uid() = user_id);
|
|
|
|
CREATE POLICY "Usuários atualizam suas próprias skills"
|
|
ON public.skills FOR UPDATE
|
|
USING (auth.uid() = user_id);
|
|
|
|
CREATE POLICY "Usuários deletam suas próprias skills"
|
|
ON public.skills FOR DELETE
|
|
USING (auth.uid() = user_id);
|
|
|
|
-- Trigger updated_at
|
|
CREATE TRIGGER trg_skills_updated_at
|
|
BEFORE UPDATE ON public.skills
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- ------------------------------------------------------------
|
|
-- 3) Tabela: skill_versions
|
|
-- ------------------------------------------------------------
|
|
CREATE TABLE public.skill_versions (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
skill_id uuid NOT NULL REFERENCES public.skills(id) ON DELETE CASCADE,
|
|
version_number int NOT NULL,
|
|
markdown_content text NOT NULL CHECK (length(markdown_content) <= 50000),
|
|
form_inputs jsonb NOT NULL,
|
|
is_live boolean NOT NULL DEFAULT false,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
created_by uuid NOT NULL REFERENCES public.profiles(id),
|
|
UNIQUE (skill_id, version_number)
|
|
);
|
|
|
|
COMMENT ON COLUMN public.skill_versions.form_inputs IS
|
|
'Snapshot dos campos do formulário usados para gerar esta versão. Schema esperado: { name: string, description: string, trigger_keywords: string, expected_inputs: string|null, steps: string, required_tools: string[], success_criteria: string, example_use_case: string|null }';
|
|
|
|
CREATE INDEX idx_skill_versions_skill_id ON public.skill_versions(skill_id);
|
|
CREATE INDEX idx_skill_versions_is_live ON public.skill_versions(skill_id) WHERE is_live = true;
|
|
|
|
-- INVARIANTE CRÍTICA: apenas 1 versão live por skill (garantido no banco)
|
|
CREATE UNIQUE INDEX skill_versions_one_live_per_skill
|
|
ON public.skill_versions(skill_id)
|
|
WHERE is_live = true;
|
|
|
|
ALTER TABLE public.skill_versions ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "Usuários veem versões das próprias skills"
|
|
ON public.skill_versions FOR SELECT
|
|
USING (EXISTS (
|
|
SELECT 1 FROM public.skills s WHERE s.id = skill_versions.skill_id AND s.user_id = auth.uid()
|
|
));
|
|
|
|
CREATE POLICY "Usuários criam versões das próprias skills"
|
|
ON public.skill_versions FOR INSERT
|
|
WITH CHECK (EXISTS (
|
|
SELECT 1 FROM public.skills s WHERE s.id = skill_versions.skill_id AND s.user_id = auth.uid()
|
|
));
|
|
|
|
CREATE POLICY "Usuários atualizam versões das próprias skills"
|
|
ON public.skill_versions FOR UPDATE
|
|
USING (EXISTS (
|
|
SELECT 1 FROM public.skills s WHERE s.id = skill_versions.skill_id AND s.user_id = auth.uid()
|
|
));
|
|
|
|
CREATE POLICY "Usuários deletam versões das próprias skills"
|
|
ON public.skill_versions FOR DELETE
|
|
USING (EXISTS (
|
|
SELECT 1 FROM public.skills s WHERE s.id = skill_versions.skill_id AND s.user_id = auth.uid()
|
|
));
|
|
|
|
-- ------------------------------------------------------------
|
|
-- 4) Tabela: skill_test_runs
|
|
-- ------------------------------------------------------------
|
|
CREATE TABLE public.skill_test_runs (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
skill_version_id uuid NOT NULL REFERENCES public.skill_versions(id) ON DELETE CASCADE,
|
|
user_id uuid NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
|
|
test_input text NOT NULL,
|
|
test_output text,
|
|
status text NOT NULL CHECK (status IN ('running', 'success', 'error')),
|
|
error_message text,
|
|
duration_ms int,
|
|
test_type text NOT NULL DEFAULT 'dry_run' CHECK (test_type IN ('dry_run', 'real')),
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_skill_test_runs_skill_version_id ON public.skill_test_runs(skill_version_id);
|
|
CREATE INDEX idx_skill_test_runs_user_id ON public.skill_test_runs(user_id);
|
|
|
|
ALTER TABLE public.skill_test_runs ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "Usuários veem seus próprios test runs"
|
|
ON public.skill_test_runs FOR SELECT
|
|
USING (auth.uid() = user_id);
|
|
|
|
CREATE POLICY "Usuários criam seus próprios test runs"
|
|
ON public.skill_test_runs FOR INSERT
|
|
WITH CHECK (auth.uid() = user_id);
|
|
|
|
-- ------------------------------------------------------------
|
|
-- 5) View: user_skill_limits
|
|
-- ------------------------------------------------------------
|
|
CREATE VIEW public.user_skill_limits
|
|
WITH (security_invoker = true)
|
|
AS
|
|
SELECT
|
|
p.id AS user_id,
|
|
pl.slug AS plan_slug,
|
|
CASE
|
|
WHEN pl.slug IS NULL THEN NULL
|
|
WHEN pl.slug = 'basic' THEN 5
|
|
WHEN pl.slug = 'starter' THEN 15
|
|
WHEN pl.slug = 'professional' THEN 50
|
|
WHEN pl.slug = 'enterprise' THEN 999999
|
|
ELSE NULL
|
|
END AS max_skills,
|
|
(
|
|
SELECT count(*)::int
|
|
FROM public.skills sk
|
|
WHERE sk.user_id = p.id AND sk.status != 'archived'
|
|
) AS current_skills_count
|
|
FROM public.profiles p
|
|
LEFT JOIN public.subscriptions s
|
|
ON s.user_id = p.id
|
|
AND s.status IN ('active', 'trialing')
|
|
LEFT JOIN public.plans pl
|
|
ON pl.id = s.plan_id;
|
|
|
|
GRANT SELECT ON public.user_skill_limits TO authenticated;
|
|
|
|
-- ------------------------------------------------------------
|
|
-- 6) Função + trigger: enforce_skill_limit (BEFORE INSERT)
|
|
-- ------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION public.enforce_skill_limit()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
v_max int;
|
|
v_count int;
|
|
BEGIN
|
|
SELECT max_skills, current_skills_count
|
|
INTO v_max, v_count
|
|
FROM public.user_skill_limits
|
|
WHERE user_id = NEW.user_id;
|
|
|
|
IF v_max IS NULL THEN
|
|
RAISE EXCEPTION 'Sem assinatura ativa para criar skills'
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
|
|
IF v_count >= v_max THEN
|
|
RAISE EXCEPTION 'Limite de skills atingido para o plano'
|
|
USING ERRCODE = 'P0002';
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
CREATE TRIGGER trigger_enforce_skill_limit
|
|
BEFORE INSERT ON public.skills
|
|
FOR EACH ROW EXECUTE FUNCTION public.enforce_skill_limit();
|
|
|
|
-- ------------------------------------------------------------
|
|
-- 7) Função + trigger: cleanup_old_skill_versions (AFTER INSERT)
|
|
-- ------------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION public.cleanup_old_skill_versions()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
DELETE FROM public.skill_versions
|
|
WHERE skill_id = NEW.skill_id
|
|
AND is_live = false
|
|
AND id NOT IN (
|
|
SELECT id FROM public.skill_versions
|
|
WHERE skill_id = NEW.skill_id
|
|
AND is_live = false
|
|
ORDER BY created_at DESC
|
|
LIMIT 12
|
|
);
|
|
RETURN NULL;
|
|
END;
|
|
$$;
|
|
|
|
CREATE TRIGGER trigger_cleanup_skill_versions
|
|
AFTER INSERT ON public.skill_versions
|
|
FOR EACH ROW EXECUTE FUNCTION public.cleanup_old_skill_versions(); |