diff --git a/database-novo/migrations/20260613000006_fix_audit_global_tables.sql b/database-novo/migrations/20260613000006_fix_audit_global_tables.sql new file mode 100644 index 0000000..7be21b5 --- /dev/null +++ b/database-novo/migrations/20260613000006_fix_audit_global_tables.sql @@ -0,0 +1,53 @@ +-- ============================================================================= +-- Fix (regressão schema-per-tenant): log_audit_change quebra INSERT em tabelas +-- GLOBAIS auditadas. +-- +-- log_audit_change deriva o tenant via tenant_id_for_schema(TG_TABLE_SCHEMA). +-- Para tabelas em tenant_ isso resolve certo. Mas o trigger também está +-- em public.tenant_members (tabela global) — e tenant_id_for_schema('public') +-- retorna NULL, violando audit_logs.tenant_id (NOT NULL). Resultado: QUALQUER +-- INSERT em tenant_members falhava (provisionamento, aceite de convite). +-- +-- Fix: quando o schema não resolve um tenant (tabela global), usa o tenant_id +-- da própria linha (tenant_members.tenant_id). Se ainda assim for NULL, não +-- audita — mas NUNCA quebra a operação de negócio. +-- ============================================================================= + +CREATE OR REPLACE FUNCTION public.log_audit_change() +RETURNS trigger +LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp' +AS $function$ +DECLARE + v_tenant_id uuid; v_entity_id text; v_old jsonb; v_new jsonb; v_changed text[]; + v_heavy text[] := ARRAY['content','content_html','content_json','raw_data','signature_data','pdf_blob','binary','body_html','body_text']; + v_noise text[] := ARRAY['updated_at','last_seen_at','last_activity_at']; +BEGIN + v_tenant_id := public.tenant_id_for_schema(TG_TABLE_SCHEMA); + + -- tabela global (public.*): cai no tenant_id da própria linha, se existir + IF v_tenant_id IS NULL THEN + v_tenant_id := NULLIF(to_jsonb(COALESCE(NEW, OLD)) ->> 'tenant_id', '')::uuid; + END IF; + + -- sem tenant resolvível → não audita, mas não quebra a operação + IF v_tenant_id IS NULL THEN + RETURN COALESCE(NEW, OLD); + END IF; + + IF TG_OP = 'DELETE' THEN + v_entity_id := OLD.id::text; v_old := to_jsonb(OLD) - v_heavy; v_new := NULL; + ELSIF TG_OP = 'INSERT' THEN + v_entity_id := NEW.id::text; v_old := NULL; v_new := to_jsonb(NEW) - v_heavy; + ELSE + v_entity_id := NEW.id::text; v_old := to_jsonb(OLD) - v_heavy; v_new := to_jsonb(NEW) - v_heavy; + SELECT array_agg(key ORDER BY key) INTO v_changed + FROM jsonb_each(to_jsonb(NEW)) AS kv(key, value) + WHERE (to_jsonb(OLD))->kv.key IS DISTINCT FROM kv.value; + IF v_changed IS NULL THEN RETURN NEW; END IF; + IF v_changed <@ v_noise THEN RETURN NEW; END IF; + END IF; + + INSERT INTO public.audit_logs (tenant_id, user_id, entity_type, entity_id, action, old_values, new_values, changed_fields) + VALUES (v_tenant_id, auth.uid(), TG_TABLE_NAME, v_entity_id, lower(TG_OP), v_old, v_new, v_changed); + RETURN COALESCE(NEW, OLD); +END $function$;