Files
agenciapsilmno/database-novo/migrations/20260612000002_f1_tenant_schema_helpers.sql
T
Leonardo 05c6746e33 schema-per-tenant: F0 categorizacao + F1 template/helpers + F2 provisionamento
- docs/F0_categorizacao.md: varredura completa (137 tabelas -> 84 tenant + 53
  global, 66 funcoes, FKs, policies, edge functions) + decisoes Q1-Q4
- F1 (migrations 01-05): tenants.slug, helpers de schema, _tenant_template
  (84 tabelas sem tenant_id, singletons, views __SCHEMA__/__TENANT_ID__),
  clone_tenant_template/drop_tenant_schema, channel_routing, tenant_schemas
- F2 (migration 06): provision_account_tenant/create_clinic_tenant/
  ensure_personal_tenant_for_user clonam schema na mesma transacao
- db.cjs: psqlFile agora usa ON_ERROR_STOP=1 (falha de migration nao passa
  mais como sucesso silencioso)
- blueprint original em novo-rumo.txt; wiki Obsidian atualizada

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:58:46 -03:00

74 lines
2.4 KiB
PL/PgSQL

-- =============================================================================
-- F1.2 — Schema-per-tenant: helpers de resolução de schema
-- Adaptação ao modelo multi-membership deste projeto (docs/F0_categorizacao.md D2):
-- profiles.tenant_id é NULL; membership vive em tenant_members (multi-tenant).
-- Logo NÃO existe current_tenant_schema() — RPCs recebem p_tenant_id explícito
-- e validam via tenant_schema_checked(p_tenant_id).
-- =============================================================================
BEGIN;
-- slug -> nome de schema (validado). Retorna NULL se slug inválido.
CREATE OR REPLACE FUNCTION public.tenant_schema_name(p_slug text)
RETURNS text
LANGUAGE sql
IMMUTABLE
AS $$
SELECT CASE
WHEN p_slug ~ '^[a-z][a-z0-9_]{1,47}$' THEN 'tenant_' || p_slug
ELSE NULL
END;
$$;
-- tenant_id -> nome de schema
CREATE OR REPLACE FUNCTION public.tenant_schema_for(p_tenant_id uuid)
RETURNS text
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $$
SELECT public.tenant_schema_name(t.slug) FROM public.tenants t WHERE t.id = p_tenant_id;
$$;
-- nome de schema -> tenant_id (CRÍTICO pra triggers: a coluna tenant_id não
-- existe mais nas tabelas tenant; o schema é a identidade)
CREATE OR REPLACE FUNCTION public.tenant_id_for_schema(p_schema text)
RETURNS uuid
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $$
SELECT t.id FROM public.tenants t WHERE public.tenant_schema_name(t.slug) = p_schema;
$$;
-- Resolve schema de um tenant COM validação de acesso do usuário logado.
-- Substitui o current_tenant_schema() do blueprint (que assumia 1 tenant/usuário).
CREATE OR REPLACE FUNCTION public.tenant_schema_checked(p_tenant_id uuid)
RETURNS text
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $$
DECLARE
v_schema text;
BEGIN
IF p_tenant_id IS NULL THEN
RAISE EXCEPTION 'tenant_schema_checked: p_tenant_id obrigatório';
END IF;
IF NOT public.is_tenant_member(p_tenant_id) AND NOT public.is_saas_admin() THEN
RAISE EXCEPTION 'acesso negado ao tenant %', p_tenant_id
USING ERRCODE = '42501';
END IF;
v_schema := public.tenant_schema_for(p_tenant_id);
IF v_schema IS NULL THEN
RAISE EXCEPTION 'tenant % não encontrado ou slug inválido', p_tenant_id;
END IF;
RETURN v_schema;
END;
$$;
COMMIT;