mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 09:16:46 +00:00
Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
parent
2c5ca61b19
commit
71d0c59d69
6 changed files with 466 additions and 0 deletions
148
supabase/functions/publish-skill-version/index.ts
Normal file
148
supabase/functions/publish-skill-version/index.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// Promove uma skill_version a "live" de forma atômica e idempotente.
|
||||
// Garantia adicional: unique index parcial skill_versions_one_live_per_skill no banco.
|
||||
// TODO Fase 5: dispatch SSH deploy to container after publish
|
||||
import { createClient } from "npm:@supabase/supabase-js@2";
|
||||
import { corsHeaders } from "../_shared/cors.ts";
|
||||
|
||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
|
||||
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
||||
|
||||
const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
// Auth
|
||||
const authHeader = req.headers.get("Authorization");
|
||||
if (!authHeader) {
|
||||
return new Response(JSON.stringify({ error: "Não autenticado" }), {
|
||||
status: 401,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
const token = authHeader.replace("Bearer ", "");
|
||||
const { data: userData, error: userErr } = await admin.auth.getUser(token);
|
||||
if (userErr || !userData?.user) {
|
||||
return new Response(JSON.stringify({ error: "Não autenticado" }), {
|
||||
status: 401,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
const userId = userData.user.id;
|
||||
|
||||
let body: { skill_version_id?: string };
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: "JSON inválido" }), {
|
||||
status: 400,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const { skill_version_id } = body;
|
||||
if (!skill_version_id) {
|
||||
return new Response(JSON.stringify({ error: "skill_version_id é obrigatório" }), {
|
||||
status: 400,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Carrega versão + skill (verifica ownership e estado atual)
|
||||
const { data: versionRow, error: vErr } = await admin
|
||||
.from("skill_versions")
|
||||
.select("id, skill_id, version_number, is_live, skills!inner(id, user_id)")
|
||||
.eq("id", skill_version_id)
|
||||
.maybeSingle();
|
||||
|
||||
if (vErr || !versionRow) {
|
||||
return new Response(JSON.stringify({ error: "Versão não encontrada" }), {
|
||||
status: 404,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
// @ts-expect-error nested
|
||||
const skillUserId: string = versionRow.skills.user_id;
|
||||
if (skillUserId !== userId) {
|
||||
return new Response(JSON.stringify({ error: "Acesso negado" }), {
|
||||
status: 403,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Idempotência: já é live, no-op
|
||||
if (versionRow.is_live === true) {
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, no_op: true, version_number: versionRow.version_number }),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const skillId: string = versionRow.skill_id;
|
||||
|
||||
// Postgres não permite transação multi-statement via supabase-js.
|
||||
// Estratégia: 1) zera todos is_live da skill, 2) marca a alvo como live, 3) atualiza skills.
|
||||
// O unique index parcial skill_versions_one_live_per_skill protege contra race.
|
||||
// Se duas execuções rodarem em paralelo, uma delas falhará no passo 2 com 23505.
|
||||
|
||||
// Passo 1: desmarcar todas as outras versões como live
|
||||
const { error: clearErr } = await admin
|
||||
.from("skill_versions")
|
||||
.update({ is_live: false })
|
||||
.eq("skill_id", skillId)
|
||||
.eq("is_live", true);
|
||||
|
||||
if (clearErr) {
|
||||
console.error("Clear live error:", clearErr);
|
||||
return new Response(JSON.stringify({ error: "Falha ao publicar (clear)." }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Passo 2: marcar a versão alvo como live
|
||||
const { error: setErr } = await admin
|
||||
.from("skill_versions")
|
||||
.update({ is_live: true })
|
||||
.eq("id", skill_version_id);
|
||||
|
||||
if (setErr) {
|
||||
// Race condition na invariante de banco
|
||||
if (setErr.code === "23505") {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Conflito de concorrência. Tente novamente." }),
|
||||
{ status: 409, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
console.error("Set live error:", setErr);
|
||||
return new Response(JSON.stringify({ error: "Falha ao publicar (set)." }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Passo 3: atualizar skills.current_version_id e status
|
||||
const { error: updSkillErr } = await admin
|
||||
.from("skills")
|
||||
.update({
|
||||
current_version_id: skill_version_id,
|
||||
status: "active",
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", skillId);
|
||||
|
||||
if (updSkillErr) {
|
||||
console.error("Update skill error:", updSkillErr);
|
||||
return new Response(JSON.stringify({ error: "Falha ao publicar (skill)." }), {
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, version_number: versionRow.version_number }),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue