Files
agenciapsilmno/database-novo/migrations/20260613000006_fix_audit_global_tables.sql
T
Leonardo 31c4f08451 fix(audit): log_audit_change quebrava INSERT em tabelas globais
Regressao do schema-per-tenant: o trigger de auditoria deriva tenant_id via
tenant_id_for_schema(TG_TABLE_SCHEMA), que retorna NULL para public.* (ex.:
tenant_members) -> violava audit_logs.tenant_id NOT NULL -> QUALQUER novo
membership (provisionamento, aceite de convite) falhava.

Fix: quando o schema nao resolve tenant, cai no tenant_id da propria linha;
se ainda NULL, nao audita mas nunca quebra a operacao.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:20:51 -03:00

54 lines
2.7 KiB
PL/PgSQL

-- =============================================================================
-- 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_<slug> 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$;