05c6746e33
- 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>
88 lines
3.0 KiB
PL/PgSQL
88 lines
3.0 KiB
PL/PgSQL
-- =============================================================================
|
|
-- F1.1 — Schema-per-tenant: coluna tenants.slug
|
|
-- Plano: novo-rumo.txt + docs/F0_categorizacao.md (decisão Q1)
|
|
-- Slug é a base do nome do schema físico: tenant_<slug>. Unico e imutável.
|
|
-- =============================================================================
|
|
|
|
BEGIN;
|
|
|
|
ALTER TABLE public.tenants ADD COLUMN IF NOT EXISTS slug text;
|
|
|
|
-- Geração de slug a partir do nome (sanitizado pra identificador Postgres)
|
|
CREATE OR REPLACE FUNCTION public.generate_tenant_slug(p_name text)
|
|
RETURNS text
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
SET search_path TO 'public', 'pg_temp'
|
|
AS $$
|
|
DECLARE
|
|
base text;
|
|
cand text;
|
|
n int := 1;
|
|
BEGIN
|
|
base := lower(coalesce(nullif(trim(p_name), ''), 'tenant'));
|
|
base := translate(base,
|
|
'áàâãäåéèêëíìîïóòôõöúùûüçñýÿ',
|
|
'aaaaaaeeeeiiiiooooouuuucnyy');
|
|
base := regexp_replace(base, '[^a-z0-9]+', '_', 'g');
|
|
base := regexp_replace(base, '^_+|_+$', '', 'g');
|
|
base := left(base, 48);
|
|
IF base = '' OR base !~ '^[a-z]' THEN
|
|
base := 't_' || base;
|
|
base := left(base, 48);
|
|
END IF;
|
|
cand := base;
|
|
WHILE EXISTS (SELECT 1 FROM public.tenants WHERE slug = cand) LOOP
|
|
n := n + 1;
|
|
cand := left(base, 44) || '_' || n;
|
|
END LOOP;
|
|
RETURN cand;
|
|
END;
|
|
$$;
|
|
|
|
-- Backfill dos tenants existentes
|
|
DO $$
|
|
DECLARE r record;
|
|
BEGIN
|
|
FOR r IN SELECT id, name FROM public.tenants WHERE slug IS NULL ORDER BY created_at, id LOOP
|
|
UPDATE public.tenants SET slug = public.generate_tenant_slug(r.name) WHERE id = r.id;
|
|
RAISE NOTICE 'tenant % -> slug %', r.id, (SELECT slug FROM public.tenants WHERE id = r.id);
|
|
END LOOP;
|
|
END $$;
|
|
|
|
ALTER TABLE public.tenants ALTER COLUMN slug SET NOT NULL;
|
|
ALTER TABLE public.tenants ADD CONSTRAINT tenants_slug_key UNIQUE (slug);
|
|
ALTER TABLE public.tenants ADD CONSTRAINT tenants_slug_format CHECK (slug ~ '^[a-z][a-z0-9_]{1,47}$');
|
|
|
|
-- Auto-gera no INSERT (provisionamento atual não conhece slug); imutável no UPDATE
|
|
CREATE OR REPLACE FUNCTION public.trg_tenants_slug()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path TO 'public', 'pg_temp'
|
|
AS $$
|
|
BEGIN
|
|
IF TG_OP = 'INSERT' THEN
|
|
IF NEW.slug IS NULL OR trim(NEW.slug) = '' THEN
|
|
NEW.slug := public.generate_tenant_slug(NEW.name);
|
|
END IF;
|
|
RETURN NEW;
|
|
END IF;
|
|
IF NEW.slug IS DISTINCT FROM OLD.slug THEN
|
|
RAISE EXCEPTION 'tenants.slug é imutável (tenant %, % -> %)', OLD.id, OLD.slug, NEW.slug;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
DROP TRIGGER IF EXISTS trg_tenants_slug_ins ON public.tenants;
|
|
CREATE TRIGGER trg_tenants_slug_ins BEFORE INSERT ON public.tenants
|
|
FOR EACH ROW EXECUTE FUNCTION public.trg_tenants_slug();
|
|
|
|
DROP TRIGGER IF EXISTS trg_tenants_slug_upd ON public.tenants;
|
|
CREATE TRIGGER trg_tenants_slug_upd BEFORE UPDATE OF slug ON public.tenants
|
|
FOR EACH ROW EXECUTE FUNCTION public.trg_tenants_slug();
|
|
|
|
COMMIT;
|