Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-04-17 18:46:52 +00:00
parent ec64a8ba0b
commit 2c5ca61b19
2 changed files with 452 additions and 1 deletions

View file

@ -59,6 +59,13 @@ export type Database = {
referencedRelation: "profiles"
referencedColumns: ["id"]
},
{
foreignKeyName: "agent_instances_user_id_fkey"
columns: ["user_id"]
isOneToOne: true
referencedRelation: "user_skill_limits"
referencedColumns: ["user_id"]
},
]
}
enterprise_leads: {
@ -214,6 +221,183 @@ export type Database = {
}
Relationships: []
}
skill_test_runs: {
Row: {
created_at: string
duration_ms: number | null
error_message: string | null
id: string
skill_version_id: string
status: string
test_input: string
test_output: string | null
test_type: string
user_id: string
}
Insert: {
created_at?: string
duration_ms?: number | null
error_message?: string | null
id?: string
skill_version_id: string
status: string
test_input: string
test_output?: string | null
test_type?: string
user_id: string
}
Update: {
created_at?: string
duration_ms?: number | null
error_message?: string | null
id?: string
skill_version_id?: string
status?: string
test_input?: string
test_output?: string | null
test_type?: string
user_id?: string
}
Relationships: [
{
foreignKeyName: "skill_test_runs_skill_version_id_fkey"
columns: ["skill_version_id"]
isOneToOne: false
referencedRelation: "skill_versions"
referencedColumns: ["id"]
},
{
foreignKeyName: "skill_test_runs_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "profiles"
referencedColumns: ["id"]
},
{
foreignKeyName: "skill_test_runs_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "user_skill_limits"
referencedColumns: ["user_id"]
},
]
}
skill_versions: {
Row: {
created_at: string
created_by: string
form_inputs: Json
id: string
is_live: boolean
markdown_content: string
skill_id: string
version_number: number
}
Insert: {
created_at?: string
created_by: string
form_inputs: Json
id?: string
is_live?: boolean
markdown_content: string
skill_id: string
version_number: number
}
Update: {
created_at?: string
created_by?: string
form_inputs?: Json
id?: string
is_live?: boolean
markdown_content?: string
skill_id?: string
version_number?: number
}
Relationships: [
{
foreignKeyName: "skill_versions_created_by_fkey"
columns: ["created_by"]
isOneToOne: false
referencedRelation: "profiles"
referencedColumns: ["id"]
},
{
foreignKeyName: "skill_versions_created_by_fkey"
columns: ["created_by"]
isOneToOne: false
referencedRelation: "user_skill_limits"
referencedColumns: ["user_id"]
},
{
foreignKeyName: "skill_versions_skill_id_fkey"
columns: ["skill_id"]
isOneToOne: false
referencedRelation: "skills"
referencedColumns: ["id"]
},
]
}
skills: {
Row: {
agent_instance_id: string
created_at: string
current_version_id: string | null
description: string
id: string
name: string
status: string
trigger_keywords: string
updated_at: string
user_id: string
}
Insert: {
agent_instance_id: string
created_at?: string
current_version_id?: string | null
description: string
id?: string
name: string
status?: string
trigger_keywords: string
updated_at?: string
user_id: string
}
Update: {
agent_instance_id?: string
created_at?: string
current_version_id?: string | null
description?: string
id?: string
name?: string
status?: string
trigger_keywords?: string
updated_at?: string
user_id?: string
}
Relationships: [
{
foreignKeyName: "skills_agent_instance_id_fkey"
columns: ["agent_instance_id"]
isOneToOne: false
referencedRelation: "agent_instances"
referencedColumns: ["id"]
},
{
foreignKeyName: "skills_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "profiles"
referencedColumns: ["id"]
},
{
foreignKeyName: "skills_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "user_skill_limits"
referencedColumns: ["user_id"]
},
]
}
stripe_webhook_events: {
Row: {
event_type: string
@ -308,11 +492,26 @@ export type Database = {
referencedRelation: "profiles"
referencedColumns: ["id"]
},
{
foreignKeyName: "subscriptions_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "user_skill_limits"
referencedColumns: ["user_id"]
},
]
}
}
Views: {
[_ in never]: never
user_skill_limits: {
Row: {
current_skills_count: number | null
max_skills: number | null
plan_slug: string | null
user_id: string | null
}
Relationships: []
}
}
Functions: {
has_active_subscription: {

View file

@ -0,0 +1,252 @@
-- ============================================================
-- 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();