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>
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
-- =============================================================================
|
||||
-- F2 — Schema-per-tenant: provisionamento cria o schema físico
|
||||
--
|
||||
-- Os 3 pontos de criação de tenant passam a chamar clone_tenant_template()
|
||||
-- logo após inserir em tenants/tenant_members. Tudo na mesma transação:
|
||||
-- se o clone falhar, o tenant não nasce (atomicidade).
|
||||
--
|
||||
-- Pontos cobertos (F0 §levantamento — não há outros INSERT INTO tenants):
|
||||
-- * provision_account_tenant — wizard de cadastro (therapist/clinic_*)
|
||||
-- * create_clinic_tenant — criação avulsa de clínica
|
||||
-- * ensure_personal_tenant_for_user — tenant pessoal (kind='saas'),
|
||||
-- chamado também pelo trigger de signup (handle_new_user_create_personal_tenant)
|
||||
--
|
||||
-- Decisão Q2: TODO tenant ganha schema, inclusive therapist e pessoal.
|
||||
-- Clones nascem sem triggers de negócio (F6) e fora do PostgREST (F5).
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.create_clinic_tenant(p_name text)
|
||||
RETURNS uuid
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
declare
|
||||
v_uid uuid;
|
||||
v_tenant uuid;
|
||||
v_name text;
|
||||
begin
|
||||
v_uid := auth.uid();
|
||||
if v_uid is null then
|
||||
raise exception 'Not authenticated';
|
||||
end if;
|
||||
|
||||
v_name := nullif(trim(coalesce(p_name, '')), '');
|
||||
if v_name is null then
|
||||
v_name := 'Clínica';
|
||||
end if;
|
||||
|
||||
insert into public.tenants (name, kind, created_at)
|
||||
values (v_name, 'clinic', now())
|
||||
returning id into v_tenant;
|
||||
|
||||
insert into public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
values (v_tenant, v_uid, 'tenant_admin', 'active', now());
|
||||
|
||||
-- F2: schema físico do tenant (rollback junto se algo falhar)
|
||||
perform public.clone_tenant_template(v_tenant);
|
||||
|
||||
return v_tenant;
|
||||
end;
|
||||
$function$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.ensure_personal_tenant_for_user(p_user_id uuid)
|
||||
RETURNS uuid
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
declare
|
||||
v_uid uuid;
|
||||
v_existing uuid;
|
||||
v_tenant uuid;
|
||||
v_email text;
|
||||
v_name text;
|
||||
begin
|
||||
v_uid := p_user_id;
|
||||
if v_uid is null then
|
||||
raise exception 'Missing user id';
|
||||
end if;
|
||||
|
||||
-- só considera tenant pessoal (kind='saas')
|
||||
select tm.tenant_id
|
||||
into v_existing
|
||||
from public.tenant_members tm
|
||||
join public.tenants t on t.id = tm.tenant_id
|
||||
where tm.user_id = v_uid
|
||||
and tm.status = 'active'
|
||||
and t.kind = 'saas'
|
||||
order by tm.created_at desc
|
||||
limit 1;
|
||||
|
||||
if v_existing is not null then
|
||||
return v_existing;
|
||||
end if;
|
||||
|
||||
select email into v_email
|
||||
from auth.users
|
||||
where id = v_uid;
|
||||
|
||||
v_name := coalesce(split_part(v_email, '@', 1), 'Conta');
|
||||
|
||||
insert into public.tenants (name, kind, created_at)
|
||||
values (v_name || ' (Pessoal)', 'saas', now())
|
||||
returning id into v_tenant;
|
||||
|
||||
insert into public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
values (v_tenant, v_uid, 'tenant_admin', 'active', now());
|
||||
|
||||
-- F2: schema físico do tenant (rollback junto se algo falhar)
|
||||
perform public.clone_tenant_template(v_tenant);
|
||||
|
||||
return v_tenant;
|
||||
end;
|
||||
$function$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.provision_account_tenant(p_user_id uuid, p_kind text, p_name text DEFAULT NULL::text)
|
||||
RETURNS uuid
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_tenant_id uuid;
|
||||
v_account_type text;
|
||||
v_name text;
|
||||
BEGIN
|
||||
IF p_kind NOT IN ('therapist', 'clinic_coworking', 'clinic_reception', 'clinic_full') THEN
|
||||
RAISE EXCEPTION 'kind inválido: "%". Use: therapist, clinic_coworking, clinic_reception, clinic_full.', p_kind
|
||||
USING ERRCODE = 'P0001';
|
||||
END IF;
|
||||
|
||||
v_account_type := CASE WHEN p_kind = 'therapist' THEN 'therapist' ELSE 'clinic' END;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM public.tenant_members tm
|
||||
JOIN public.tenants t ON t.id = tm.tenant_id
|
||||
WHERE tm.user_id = p_user_id
|
||||
AND tm.role = 'tenant_admin'
|
||||
AND tm.status = 'active'
|
||||
AND t.kind = p_kind
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Usuário já possui um tenant do tipo "%".', p_kind
|
||||
USING ERRCODE = 'P0001';
|
||||
END IF;
|
||||
|
||||
v_name := COALESCE(
|
||||
NULLIF(TRIM(p_name), ''),
|
||||
(
|
||||
SELECT COALESCE(NULLIF(TRIM(pr.full_name), ''), SPLIT_PART(au.email, '@', 1))
|
||||
FROM public.profiles pr
|
||||
JOIN auth.users au ON au.id = pr.id
|
||||
WHERE pr.id = p_user_id
|
||||
),
|
||||
'Conta'
|
||||
);
|
||||
|
||||
INSERT INTO public.tenants (name, kind, created_at)
|
||||
VALUES (v_name, p_kind, now())
|
||||
RETURNING id INTO v_tenant_id;
|
||||
|
||||
INSERT INTO public.tenant_members (tenant_id, user_id, role, status, created_at)
|
||||
VALUES (v_tenant_id, p_user_id, 'tenant_admin', 'active', now());
|
||||
|
||||
UPDATE public.profiles
|
||||
SET account_type = v_account_type
|
||||
WHERE id = p_user_id;
|
||||
|
||||
PERFORM public.seed_determined_commitments(v_tenant_id);
|
||||
|
||||
-- F2: schema físico do tenant (rollback junto se algo falhar)
|
||||
PERFORM public.clone_tenant_template(v_tenant_id);
|
||||
|
||||
RETURN v_tenant_id;
|
||||
END;
|
||||
$function$;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user