F3 schema-per-tenant: frontend usa tenantDb() pra tabelas tenant

- useTenantDb composable + lib/supabase/tenantClient (tenantDb/tenantSchemaName)
- tenantStore: getters activeTenantSlug/activeTenantSchema; my_tenants() RPC
  passa a devolver slug+nome (migration 07)
- codemod scripts/codemod-tenant-db.py: supabase.from('<84 tabelas + 6 views
  tenant>') -> tenantDb().from(...) em 139 arquivos (777 chamadas), remove
  .eq('tenant_id') das cadeias tenant (173)
- passada manual (4 agentes): remove tenant_id de payloads insert/upsert/update,
  selects, .or/.is de defaults; onConflict ajustado pros uniques sem tenant_id
  (singletons usam 'singleton'); realtime de tabelas tenant aponta pro schema
  do tenant ativo; repos dropam tenant_id defensivamente de payloads externos
- agendaSelects: tenant_id fora do AGENDA_EVENT_SELECT (quebraria PostgREST)
- zero embeds cross-schema (todos FK embeds sao tenant->tenant ou global->global)
- build de producao passa; 67 .js checados

Pendente (fora do escopo F3, sao cross-tenant/anon -> F4/F6):
- AgendadorPublicoPage (anon, resolve tenant por link_slug)
- Saas{Feriados,NotificationTemplates,DocumentTemplates,Whatsapp}Page
  (gerenciam defaults do sistema / views cross-tenant)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-06-13 04:44:59 -03:00
parent 05c6746e33
commit a7f6bcbe66
142 changed files with 1404 additions and 1472 deletions
+10 -15
View File
@@ -11,6 +11,7 @@
import { ref } from 'vue';
import { supabase } from '@/lib/supabase/client';
import { tenantDb } from '@/lib/supabase/tenantClient';
import { useTenantStore } from '@/stores/tenantStore';
const EMAIL_RE = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
@@ -37,9 +38,8 @@ export function useContactEmails() {
async function loadTypes() {
try {
const { data } = await supabase
.from('contact_email_types')
.select('id, tenant_id, name, slug, icon, is_system, position')
const { data } = await tenantDb().from('contact_email_types')
.select('id, name, slug, icon, is_system, position')
.order('position', { ascending: true })
.order('name', { ascending: true });
types.value = data || [];
@@ -56,8 +56,7 @@ export function useContactEmails() {
}
loading.value = true;
try {
const { data, error } = await supabase
.from('contact_emails')
const { data, error } = await tenantDb().from('contact_emails')
.select('id, contact_email_type_id, email, is_primary, notes, position, created_at')
.eq('entity_type', entityType)
.eq('entity_id', entityId)
@@ -74,8 +73,7 @@ export function useContactEmails() {
}
async function unsetOtherPrimaries(entityType, entityId, exceptId = null) {
const q = supabase
.from('contact_emails')
const q = tenantDb().from('contact_emails')
.update({ is_primary: false })
.eq('entity_type', entityType)
.eq('entity_id', entityId)
@@ -122,10 +120,8 @@ export function useContactEmails() {
}
const maxPos = emails.value.reduce((m, e) => Math.max(m, e.position || 0), 0);
const { data, error } = await supabase
.from('contact_emails')
const { data, error } = await tenantDb().from('contact_emails')
.insert({
tenant_id: tenantId,
entity_type: entityType,
entity_id: entityId,
contact_email_type_id,
@@ -172,7 +168,7 @@ export function useContactEmails() {
if (sanitized.is_primary === true) {
await unsetOtherPrimaries(entityType, entityId, id);
}
const { error } = await supabase.from('contact_emails').update(sanitized).eq('id', id);
const { error } = await tenantDb().from('contact_emails').update(sanitized).eq('id', id);
if (error) throw error;
await loadEmails(entityType, entityId);
return { ok: true };
@@ -200,12 +196,12 @@ export function useContactEmails() {
saving.value = true;
try {
const wasPrimary = emails.value.find((e) => e.id === id)?.is_primary;
const { error } = await supabase.from('contact_emails').delete().eq('id', id);
const { error } = await tenantDb().from('contact_emails').delete().eq('id', id);
if (error) throw error;
if (wasPrimary) {
const remaining = emails.value.filter((e) => e.id !== id).sort((a, b) => (a.position || 0) - (b.position || 0));
if (remaining.length > 0) {
await supabase.from('contact_emails').update({ is_primary: true }).eq('id', remaining[0].id);
await tenantDb().from('contact_emails').update({ is_primary: true }).eq('id', remaining[0].id);
}
}
await loadEmails(entityType, entityId);
@@ -227,7 +223,6 @@ export function useContactEmails() {
saving.value = true;
try {
const rows = pendingItems.map((e) => ({
tenant_id: tenantId,
entity_type: entityType,
entity_id: entityId,
contact_email_type_id: e.contact_email_type_id,
@@ -236,7 +231,7 @@ export function useContactEmails() {
notes: e.notes || null,
position: e.position
}));
const { error } = await supabase.from('contact_emails').insert(rows);
const { error } = await tenantDb().from('contact_emails').insert(rows);
if (error) throw error;
await loadEmails(entityType, entityId);
return { ok: true, count: rows.length };