@fayz-ai/db 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migrations/011_leadcontrol_joins_the_family.sql +5 -1
- package/migrations/029_o_administrador_enxerga_o_que_existe.sql +167 -0
- package/migrations/031_ticketcontrol_joins_the_family.sql +109 -0
- package/migrations/032_a_marca_e_um_conjunto_de_tokens.sql +291 -0
- package/migrations/041_a_entrega_tem_onde_guardar_a_politica.sql +53 -0
- package/migrations/042_o_token_de_marca_fecha_a_porta_anon.sql +17 -0
- package/migrations/043_fiscalcontrol_joins_the_family.sql +75 -0
- package/migrations/044_o_teste_gratis_dura_trinta_dias.sql +15 -0
- package/migrations/045_o_espelho_manda_enquanto_o_v2_nao_escreveu.sql +519 -0
- package/migrations/046_the_tick_advances_the_flows.sql +77 -0
- package/migrations/047_the_tick_sweeps_the_offers.sql +74 -0
- package/migrations/052_fullcontrol_joins_the_family.sql +213 -0
- package/migrations/053_o_erp_para_de_exigir_o_que_nao_desenha.sql +37 -0
- package/migrations/054_o_arquivo_migrado_ganha_bytes.sql +439 -0
- package/migrations/061_a_linha_sem_data_pode_ser_estrutura.sql +253 -0
- package/migrations/063_o_anexo_ja_registrado_recebe_os_bytes.sql +303 -0
- package/migrations/064_um_arquivo_anexado_a_dezenove_contas.sql +178 -0
- package/migrations/065_apagar_o_sobrevivente_nao_apaga_o_tenant.sql +59 -0
- package/migrations/066_the_port_machinery_closes_its_doors.sql +51 -0
- package/migrations/067_the_erp_keeps_the_front_door.sql +56 -0
- package/migrations/068_o_ledger_encontra_a_linha_pelo_caminho_que_o_writer_usa.sql +55 -0
- package/migrations/069_a_sondagem_de_irma_usa_indice.sql +293 -0
- package/migrations/071_o_sdr_trabalha_o_funil_e_mais_nada.sql +110 -0
- package/migrations/072_o_porte_nao_avisa_a_plataforma_de_uma_venda_antiga.sql +91 -0
- package/migrations/073_a_exclusao_e_contrato_do_cliente_nao_do_cluster.sql +300 -0
- package/migrations/074_a_classificacao_quebrada_nao_leva_o_dinheiro_junto.sql +332 -0
- package/migrations/075_o_apelido_da_referencia_nao_disputa_com_a_variavel.sql +276 -0
- package/migrations/076_o_desligado_tambem_precisa_atravessar.sql +110 -0
- package/migrations/077_a_unidade_escolhida_chega_ao_banco.sql +128 -0
- package/migrations/078_onde_o_profissional_atende_atravessa.sql +71 -0
- package/migrations/079_quem_aparece_na_agenda_e_quem_atende.sql +210 -0
- package/migrations/080_o_contexto_da_requisicao_se_calcula_uma_vez.sql +240 -0
- package/migrations/081_a_permissao_por_unidade_se_resolve_uma_vez_por_unidade.sql +88 -0
- package/migrations/082_a_cerca_tambem_se_lembra.sql +110 -0
- package/migrations/083_quem_e_profissional_continua_agendavel.sql +32 -0
- package/migrations/084_quem_atende_em_varias_nao_cabe_numa_coluna.sql +60 -0
- package/migrations/085_a_unidade_do_profissional_vem_de_onde_ele_atende.sql +47 -0
- package/migrations/086_a_ponte_de_clientes_volta_a_existir.sql +89 -0
- package/migrations/087_a_liberacao_pergunta_ao_indice_antes_da_funcao.sql +79 -0
- package/migrations/088_quem_pode_executar_o_que_o_porte_criou.sql +50 -0
- package/migrations/089_a_memoria_de_transacao_nao_serve_para_isto.sql +86 -0
- package/migrations/090_a_marca_tambem_conta_como_mudanca_de_configuracao.sql +75 -0
- package/migrations/091_o_shell_tambem_e_configuracao_do_tenant.sql +71 -0
- package/migrations/092_a_marca_tem_uma_grafia_so_e_uma_porta_so.sql +159 -0
- package/migrations/093_a_marca_carrega_o_tema_inteiro.sql +179 -0
- package/migrations/094_a_porta_do_dominio_aprende_a_grafia_nova.sql +55 -0
- package/migrations/095_o_aplicativo_tem_um_preset_que_a_conta_estende.sql +98 -0
- package/migrations/096_o_sublinhado_tambem_e_nome_de_modulo.sql +100 -0
- package/migrations/097_o_oficio_diz_o_que_oferece_e_a_conta_o_que_usa.sql +137 -0
- package/migrations/098_a_arvore_do_oficio_nomeia_quem_ainda_desenha.sql +22 -0
- package/migrations/099_o_estoque_mora_dentro_de_produtos.sql +48 -0
- package/migrations/100_o_chefcontrol_desenha_abas_de_modulo.sql +24 -0
- package/migrations/101_o_oficio_agrupa_o_plugin_nomeia.sql +93 -0
- package/migrations/102_tres_marcas_ganham_fundo_e_contraste.sql +73 -0
- package/migrations/103_great_djs_e_iam_club_entram_na_frota.sql +75 -0
- package/migrations/104_o_curso_e_a_bilheteria_ganham_preset.sql +29 -0
- package/migrations/105_o_que_o_oficio_exige_ele_tambem_oferece.sql +78 -0
- package/migrations/106_a_comunidade_se_gere_como_escola.sql +84 -0
- package/migrations/107_o_logo_da_coluna_e_projecao_do_documento.sql +53 -0
- package/migrations/108_o_modulo_do_oficio_vizinho_tem_onde_ser_visto.sql +78 -0
- package/migrations/109_a_conta_diz_o_que_nao_usa_e_a_marca_pinta_a_casa.sql +67 -0
- package/migrations/110_a_tabela_de_ramos_aprende_o_vocabulario_fechado.sql +60 -0
- package/migrations/111_o_erp_generico_nao_vende_por_pedido.sql +50 -0
- package/migrations/112_o_cache_do_contexto_sabe_de_quem_ele_e.sql +327 -0
- package/migrations/113_o_ramo_volta_a_saber_que_papeis_a_conta_nasce_tendo.sql +124 -0
- package/migrations/114_a_resposta_inicial_volta_a_dizer_o_que_a_pessoa_pode.sql +93 -0
- package/migrations/115_o_rail_e_de_quem_tem_um.sql +108 -0
- package/package.json +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
-- Quem aparece na agenda é quem atende.
|
|
2
|
+
--
|
|
3
|
+
-- A agenda de uma unidade mostrava gente demais, e a maior parte do excesso
|
|
4
|
+
-- não era profissional. Medido em 20/09/2026, agenda do Millenium:
|
|
5
|
+
--
|
|
6
|
+
-- 23 funcionários administrativos
|
|
7
|
+
-- 15 profissionais (o V1 diz 11)
|
|
8
|
+
-- 1 cliente
|
|
9
|
+
-- ──
|
|
10
|
+
-- 38 colunas, onde deveriam ser 11
|
|
11
|
+
--
|
|
12
|
+
-- ── A causa ─────────────────────────────────────────────────────────────────
|
|
13
|
+
--
|
|
14
|
+
-- `public.people.is_bookable` tem `DEFAULT true`, e `migration.upsert_person`
|
|
15
|
+
-- nunca o define. Então TODA pessoa que o porte escreve nasce agendável —
|
|
16
|
+
-- inclusive os 119.521 clientes, que só não aparecem porque a agenda filtra
|
|
17
|
+
-- por `kind = 'staff'`.
|
|
18
|
+
--
|
|
19
|
+
-- O V1 distingue: quem atende cliente é `professionals`; `employees` é
|
|
20
|
+
-- administrativo e nunca vira coluna na agenda de lá. A informação existia na
|
|
21
|
+
-- origem e não atravessava.
|
|
22
|
+
--
|
|
23
|
+
-- ── Declarado, não derivado ─────────────────────────────────────────────────
|
|
24
|
+
--
|
|
25
|
+
-- O writer já recebe `role` em `writer_args`, e seria fácil escrever
|
|
26
|
+
-- `is_bookable := (v_role = 'professional')` aqui dentro. Não é o que esta
|
|
27
|
+
-- migration faz.
|
|
28
|
+
--
|
|
29
|
+
-- A regra vai para `writer_args.bookable`, por tabela de origem, porque ela é
|
|
30
|
+
-- uma decisão do POOL e não do motor: um cliente onde a recepção também atende
|
|
31
|
+
-- precisa poder dizer isso sem que alguém reescreva o writer. Ausente, o
|
|
32
|
+
-- destino decide — o comportamento de antes, para todo pool que já roda.
|
|
33
|
+
--
|
|
34
|
+
-- Idempotente.
|
|
35
|
+
|
|
36
|
+
CREATE OR REPLACE FUNCTION migration.upsert_person(p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb)
|
|
37
|
+
RETURNS jsonb
|
|
38
|
+
LANGUAGE plpgsql
|
|
39
|
+
SECURITY DEFINER
|
|
40
|
+
SET search_path TO ''
|
|
41
|
+
AS $function$
|
|
42
|
+
DECLARE
|
|
43
|
+
v_role text := coalesce(p_args ->> 'role', 'customer');
|
|
44
|
+
v_doc text := public.people_normalize_document(p_row ->> 'document_number');
|
|
45
|
+
v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
|
|
46
|
+
v_origin jsonb;
|
|
47
|
+
v_existing_tenant uuid;
|
|
48
|
+
v_tombstone uuid;
|
|
49
|
+
v_candidates uuid[];
|
|
50
|
+
v_n integer;
|
|
51
|
+
v_survivor uuid;
|
|
52
|
+
v_standalone boolean := migration._standalone();
|
|
53
|
+
BEGIN
|
|
54
|
+
IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
|
|
55
|
+
IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN
|
|
56
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'a person needs an id and a name');
|
|
57
|
+
END IF;
|
|
58
|
+
IF v_role NOT IN ('customer', 'professional', 'employee', 'contact', 'supplier', 'lead') THEN
|
|
59
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', format('unknown person role %s', v_role));
|
|
60
|
+
END IF;
|
|
61
|
+
v_origin := coalesce(v_meta -> 'migration', '{}'::jsonb) - 'merged_from';
|
|
62
|
+
|
|
63
|
+
SELECT tenant_id, merged_into_id INTO v_existing_tenant, v_tombstone FROM public.people WHERE id = p_id;
|
|
64
|
+
IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN
|
|
65
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('person %s already exists in another tenant', p_id));
|
|
66
|
+
END IF;
|
|
67
|
+
IF v_tombstone IS NOT NULL THEN
|
|
68
|
+
-- the row was merged in V2 after it landed: the human's decision stands, the source now maps to the survivor
|
|
69
|
+
PERFORM migration._ensure_role(p_tenant, v_tombstone, v_role);
|
|
70
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', v_tombstone, 'reason', 'merged', 'merged', true);
|
|
71
|
+
END IF;
|
|
72
|
+
|
|
73
|
+
IF v_existing_tenant IS NOT NULL THEN
|
|
74
|
+
-- the same source row again (delta): mapped columns override, the rest stays
|
|
75
|
+
UPDATE public.people SET
|
|
76
|
+
name = p_row ->> 'name',
|
|
77
|
+
email = CASE WHEN p_row ? 'email' THEN p_row ->> 'email' ELSE email END,
|
|
78
|
+
phone = CASE WHEN p_row ? 'phone' THEN p_row ->> 'phone' ELSE phone END,
|
|
79
|
+
document_number = CASE WHEN p_row ? 'document_number' THEN v_doc ELSE document_number END,
|
|
80
|
+
avatar_url = CASE WHEN p_row ? 'avatar_url' THEN p_row ->> 'avatar_url' ELSE avatar_url END,
|
|
81
|
+
date_of_birth = CASE WHEN p_row ? 'date_of_birth' THEN (p_row ->> 'date_of_birth')::date ELSE date_of_birth END,
|
|
82
|
+
address = CASE WHEN p_row ? 'address' THEN p_row ->> 'address' ELSE address END,
|
|
83
|
+
city = CASE WHEN p_row ? 'city' THEN p_row ->> 'city' ELSE city END,
|
|
84
|
+
state = CASE WHEN p_row ? 'state' THEN p_row ->> 'state' ELSE state END,
|
|
85
|
+
country = CASE WHEN p_row ? 'country' THEN p_row ->> 'country' ELSE country END,
|
|
86
|
+
postal_code = CASE WHEN p_row ? 'postal_code' THEN p_row ->> 'postal_code' ELSE postal_code END,
|
|
87
|
+
notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,
|
|
88
|
+
is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,
|
|
89
|
+
metadata = migration._merge_meta(metadata, v_meta)
|
|
90
|
+
WHERE id = p_id;
|
|
91
|
+
PERFORM migration._ensure_role(p_tenant, p_id, v_role);
|
|
92
|
+
PERFORM migration._set_unit('public.people', p_id, (p_row ->> 'unit_id')::uuid);
|
|
93
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.people', p_id, jsonb_build_object('reason', 'updated')); END IF;
|
|
94
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'updated');
|
|
95
|
+
END IF;
|
|
96
|
+
|
|
97
|
+
IF v_doc IS NOT NULL THEN
|
|
98
|
+
SELECT array_agg(id) INTO v_candidates FROM public.people
|
|
99
|
+
WHERE tenant_id = p_tenant AND document_number = v_doc AND merged_into_id IS NULL;
|
|
100
|
+
ELSIF nullif(btrim(coalesce(p_row ->> 'email', '')), '') IS NOT NULL AND nullif(public.fayz_digits(p_row ->> 'phone'), '') IS NOT NULL THEN
|
|
101
|
+
SELECT array_agg(id) INTO v_candidates FROM public.people
|
|
102
|
+
WHERE tenant_id = p_tenant AND merged_into_id IS NULL AND lower(email) = lower(p_row ->> 'email')
|
|
103
|
+
AND public.fayz_digits(phone) = public.fayz_digits(p_row ->> 'phone');
|
|
104
|
+
END IF;
|
|
105
|
+
v_n := coalesce(array_length(v_candidates, 1), 0);
|
|
106
|
+
|
|
107
|
+
IF v_doc IS NOT NULL AND v_n >= 2 THEN
|
|
108
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'identity',
|
|
109
|
+
'reason', format('ambiguous document: %s existing people share document %s', v_n, v_doc), 'candidates', to_jsonb(v_candidates));
|
|
110
|
+
END IF;
|
|
111
|
+
IF v_doc IS NULL AND v_n >= 1 THEN
|
|
112
|
+
RETURN jsonb_build_object('status', 'quarantined', 'kind', 'identity',
|
|
113
|
+
'reason', format('no document: %s existing person(s) share the same email and phone — merge only by exact document', v_n), 'candidates', to_jsonb(v_candidates));
|
|
114
|
+
END IF;
|
|
115
|
+
|
|
116
|
+
IF v_doc IS NOT NULL AND v_n = 1 THEN
|
|
117
|
+
v_survivor := v_candidates[1];
|
|
118
|
+
UPDATE public.people SET
|
|
119
|
+
email = coalesce(email, p_row ->> 'email'),
|
|
120
|
+
phone = coalesce(phone, p_row ->> 'phone'),
|
|
121
|
+
avatar_url = coalesce(avatar_url, p_row ->> 'avatar_url'),
|
|
122
|
+
date_of_birth = coalesce(date_of_birth, (p_row ->> 'date_of_birth')::date),
|
|
123
|
+
address = coalesce(address, p_row ->> 'address'),
|
|
124
|
+
city = coalesce(city, p_row ->> 'city'),
|
|
125
|
+
state = coalesce(state, p_row ->> 'state'),
|
|
126
|
+
country = coalesce(country, p_row ->> 'country'),
|
|
127
|
+
postal_code = coalesce(postal_code, p_row ->> 'postal_code'),
|
|
128
|
+
notes = coalesce(notes, p_row ->> 'notes'),
|
|
129
|
+
metadata = migration._merge_meta(metadata,
|
|
130
|
+
(v_meta - 'migration') || jsonb_build_object('migration', jsonb_build_object('merged_from', jsonb_build_array(v_origin))))
|
|
131
|
+
WHERE id = v_survivor;
|
|
132
|
+
PERFORM migration._ensure_role(p_tenant, v_survivor, v_role);
|
|
133
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.people', v_survivor, jsonb_build_object('reason', 'merged', 'origin', v_origin)); END IF;
|
|
134
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', v_survivor, 'reason', 'merged', 'merged', true);
|
|
135
|
+
END IF;
|
|
136
|
+
|
|
137
|
+
-- kind is written for compatibility only; the role row below is the truth
|
|
138
|
+
-- (fayz.people_kind_sync = on keeps the kind→role trigger quiet meanwhile)
|
|
139
|
+
PERFORM set_config('fayz.people_kind_sync', 'on', true);
|
|
140
|
+
-- `is_bookable` decide quem vira COLUNA na agenda, e a coluna do destino tem
|
|
141
|
+
-- DEFAULT true — então, sem isto, toda pessoa migrada nasce agendável. O
|
|
142
|
+
-- papel da origem é quem sabe: no V1 quem atende cliente é `professionals`;
|
|
143
|
+
-- `employees` é administrativo e nunca aparece na agenda de lá.
|
|
144
|
+
--
|
|
145
|
+
-- É DECLARADO em `writer_args.bookable`, por tabela de origem, e não
|
|
146
|
+
-- derivado do papel aqui dentro: um pool onde a recepção também atende deve
|
|
147
|
+
-- poder dizer isso sem reescrever o writer. Ausente, o destino decide — que
|
|
148
|
+
-- é o comportamento de antes.
|
|
149
|
+
INSERT INTO public.people (id, tenant_id, kind, name, email, phone, document_number, avatar_url, date_of_birth,
|
|
150
|
+
address, city, state, country, postal_code, notes, is_active, is_bookable, metadata)
|
|
151
|
+
VALUES (p_id, p_tenant, coalesce(public.person_role_to_kind(v_role), v_role), p_row ->> 'name', p_row ->> 'email', p_row ->> 'phone', v_doc,
|
|
152
|
+
p_row ->> 'avatar_url', (p_row ->> 'date_of_birth')::date, p_row ->> 'address', p_row ->> 'city', p_row ->> 'state',
|
|
153
|
+
coalesce(p_row ->> 'country', 'BR'), p_row ->> 'postal_code', p_row ->> 'notes',
|
|
154
|
+
coalesce((p_row ->> 'is_active')::boolean, true),
|
|
155
|
+
coalesce((p_args ->> 'bookable')::boolean, true), v_meta);
|
|
156
|
+
PERFORM set_config('fayz.people_kind_sync', 'off', true);
|
|
157
|
+
PERFORM migration._ensure_role(p_tenant, p_id, v_role);
|
|
158
|
+
PERFORM migration._set_unit('public.people', p_id, (p_row ->> 'unit_id')::uuid);
|
|
159
|
+
IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.people', p_id, jsonb_build_object('reason', 'created')); END IF;
|
|
160
|
+
RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', 'created');
|
|
161
|
+
END $function$
|
|
162
|
+
|
|
163
|
+
;
|
|
164
|
+
|
|
165
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
-- A declaração, por tabela de origem
|
|
167
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
SET LOCAL migration.allowlist_unlock = 'on';
|
|
170
|
+
|
|
171
|
+
UPDATE migration.allowlist
|
|
172
|
+
SET writer_args = coalesce(writer_args, '{}'::jsonb) || jsonb_build_object('bookable', true)
|
|
173
|
+
WHERE source_table = 'professionals';
|
|
174
|
+
|
|
175
|
+
UPDATE migration.allowlist
|
|
176
|
+
SET writer_args = coalesce(writer_args, '{}'::jsonb) || jsonb_build_object('bookable', false)
|
|
177
|
+
WHERE source_table IN ('employees', 'clients', 'contacts');
|
|
178
|
+
|
|
179
|
+
RESET migration.allowlist_unlock;
|
|
180
|
+
|
|
181
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
182
|
+
-- E o que já entrou
|
|
183
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
--
|
|
185
|
+
-- Em LOTES, e não num UPDATE só: a junção com o ledger sobre 120 mil pessoas
|
|
186
|
+
-- não termina dentro do `statement_timeout`, e um reparo que estoura desfaz a
|
|
187
|
+
-- migration inteira junto. Cada volta é uma transação curta.
|
|
188
|
+
--
|
|
189
|
+
-- Só o que o PORTE escreveu — a condição exige linha no `migration.ledger`.
|
|
190
|
+
-- Alguém que o V2 tenha marcado como agendável à mão fica como está: isso é
|
|
191
|
+
-- decisão de quem usa o sistema, não do porte.
|
|
192
|
+
|
|
193
|
+
DO $$
|
|
194
|
+
DECLARE n integer;
|
|
195
|
+
BEGIN
|
|
196
|
+
LOOP
|
|
197
|
+
UPDATE public.people p
|
|
198
|
+
SET is_bookable = false
|
|
199
|
+
WHERE p.ctid IN (
|
|
200
|
+
SELECT p2.ctid FROM public.people p2
|
|
201
|
+
JOIN migration.ledger l
|
|
202
|
+
ON l.target_id = p2.id
|
|
203
|
+
AND l.target_table = 'public.people'
|
|
204
|
+
AND l.source_table IN ('employees', 'clients', 'contacts')
|
|
205
|
+
WHERE p2.is_bookable
|
|
206
|
+
LIMIT 2000);
|
|
207
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
208
|
+
EXIT WHEN n = 0;
|
|
209
|
+
END LOOP;
|
|
210
|
+
END $$;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
-- O contexto da requisição se calcula uma vez, não uma vez por linha.
|
|
2
|
+
--
|
|
3
|
+
-- Os painéis não carregavam: seis dos sete read models estouravam o
|
|
4
|
+
-- `statement_timeout`. A causa não estava nos relatórios — estava embaixo
|
|
5
|
+
-- deles.
|
|
6
|
+
--
|
|
7
|
+
-- ── A medição ───────────────────────────────────────────────────────────────
|
|
8
|
+
--
|
|
9
|
+
-- Contar UM MÊS de agenda, como `authenticated`, tenant Espaço Facial,
|
|
10
|
+
-- 20/09/2026:
|
|
11
|
+
--
|
|
12
|
+
-- select count(*) from public.appointments
|
|
13
|
+
-- where starts_at >= '2026-08-01' and starts_at < '2026-09-01'
|
|
14
|
+
-- → 8.011 linhas em 21.947 ms
|
|
15
|
+
--
|
|
16
|
+
-- Note que é a TABELA CRUA, sem view nenhuma: **2,7 ms por linha**, tudo em
|
|
17
|
+
-- RLS. E dentro dela:
|
|
18
|
+
--
|
|
19
|
+
-- app.has_unit(unidade) 1,558 ms por chamada
|
|
20
|
+
-- app.has_permission(chave, unidade) 1,002 ms
|
|
21
|
+
-- app.owner_scoped(tipo) 0,408 ms
|
|
22
|
+
--
|
|
23
|
+
-- `app.has_unit` sozinho chama `app.current_unit_id()` DUAS vezes,
|
|
24
|
+
-- `app.unit_scoping_on()` e, dentro dela, `app.current_tenant_id()`. Cada uma
|
|
25
|
+
-- com consulta própria — e todas respondem a MESMA COISA durante a requisição
|
|
26
|
+
-- inteira: quem é o usuário, que tenant ele escolheu, que unidade está em
|
|
27
|
+
-- foco, se a rede usa escopo por unidade.
|
|
28
|
+
--
|
|
29
|
+
-- Calcular isso por linha é o desperdício. Oito mil linhas × quatro perguntas
|
|
30
|
+
-- cuja resposta não muda.
|
|
31
|
+
--
|
|
32
|
+
-- Vale dizer o que piorou: a migration 073, de hoje, ensinou
|
|
33
|
+
-- `current_unit_id()` a ler o cabeçalho `x-fayz-unit` — e com isso acrescentou
|
|
34
|
+
-- uma consulta a `app.user_unit_access` a cada chamada. O conserto estava
|
|
35
|
+
-- certo; o preço dele é esta migration que paga.
|
|
36
|
+
--
|
|
37
|
+
-- ── A forma: invólucro, não reescrita ───────────────────────────────────────
|
|
38
|
+
--
|
|
39
|
+
-- As três funções mantêm o corpo EXATO que tinham, renomeadas para
|
|
40
|
+
-- `app._*_raw()`. Por cima delas entra um invólucro que memoiza em um GUC
|
|
41
|
+
-- transacional (`set_config(..., true)`).
|
|
42
|
+
--
|
|
43
|
+
-- Não reescrevi a lógica de propósito: ela decide quem vê o quê, e uma
|
|
44
|
+
-- otimização não é hora de mexer em regra de acesso. O que muda é QUANTAS
|
|
45
|
+
-- VEZES ela roda, não o que ela responde.
|
|
46
|
+
--
|
|
47
|
+
-- ── Por que memoizar é seguro aqui ──────────────────────────────────────────
|
|
48
|
+
--
|
|
49
|
+
-- O GUC é LOCAL: morre no fim da transação, e o PostgREST abre uma transação
|
|
50
|
+
-- por requisição, com JWT e cabeçalhos fixos. Não há como a resposta mudar
|
|
51
|
+
-- dentro do escopo do cache.
|
|
52
|
+
--
|
|
53
|
+
-- O sentinela `'-'` distingue "já calculei e deu NULL" de "ainda não
|
|
54
|
+
-- calculei". Sem ele o caso NULL — que é o mais comum, "todas as unidades" —
|
|
55
|
+
-- nunca seria memoizado, e o cache não serviria justamente para quem mais
|
|
56
|
+
-- precisa.
|
|
57
|
+
--
|
|
58
|
+
-- Idempotente.
|
|
59
|
+
|
|
60
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
-- 1. O corpo original, renomeado e intocado
|
|
62
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
CREATE OR REPLACE FUNCTION app._current_tenant_id_raw()
|
|
65
|
+
RETURNS uuid
|
|
66
|
+
LANGUAGE plpgsql
|
|
67
|
+
STABLE SECURITY DEFINER
|
|
68
|
+
SET search_path TO ''
|
|
69
|
+
AS $function$
|
|
70
|
+
DECLARE
|
|
71
|
+
v_uid uuid := auth.uid();
|
|
72
|
+
v_jwt jsonb := coalesce(auth.jwt(), '{}'::jsonb);
|
|
73
|
+
v_raw text;
|
|
74
|
+
v_tenant uuid;
|
|
75
|
+
v_n integer;
|
|
76
|
+
BEGIN
|
|
77
|
+
-- ① a claim assinada, se existir. Inalterado desde 051.
|
|
78
|
+
IF jsonb_exists(v_jwt, 'tenant_id') THEN
|
|
79
|
+
v_raw := v_jwt ->> 'tenant_id';
|
|
80
|
+
-- Presença expressa intenção de estreitar; valor ilegível é nenhum tenant.
|
|
81
|
+
IF v_raw IS NULL OR v_raw !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' THEN
|
|
82
|
+
RETURN NULL;
|
|
83
|
+
END IF;
|
|
84
|
+
v_tenant := v_raw::uuid;
|
|
85
|
+
IF v_uid IS NULL THEN
|
|
86
|
+
RETURN v_tenant;
|
|
87
|
+
END IF;
|
|
88
|
+
IF EXISTS (SELECT 1 FROM app.memberships m
|
|
89
|
+
WHERE m.tenant_id = v_tenant AND m.user_id = v_uid AND m.active) THEN
|
|
90
|
+
RETURN v_tenant;
|
|
91
|
+
END IF;
|
|
92
|
+
RETURN NULL;
|
|
93
|
+
END IF;
|
|
94
|
+
|
|
95
|
+
IF v_uid IS NULL THEN
|
|
96
|
+
RETURN NULL;
|
|
97
|
+
END IF;
|
|
98
|
+
|
|
99
|
+
-- ② o cabeçalho da requisição. A MESMA validação de filiação de ①: ele
|
|
100
|
+
-- escolhe entre as contas do usuário, não concede nenhuma. Sem sessão não há
|
|
101
|
+
-- o que validar, então este braço exige v_uid — que já está garantido acima.
|
|
102
|
+
v_tenant := app.requested_tenant();
|
|
103
|
+
IF v_tenant IS NOT NULL THEN
|
|
104
|
+
IF EXISTS (SELECT 1 FROM app.memberships m
|
|
105
|
+
WHERE m.tenant_id = v_tenant AND m.user_id = v_uid AND m.active) THEN
|
|
106
|
+
RETURN v_tenant;
|
|
107
|
+
END IF;
|
|
108
|
+
-- Cabeçalho apontando para conta que não é sua: nega, não recua. Recuar
|
|
109
|
+
-- para "sua única conta" transformaria um pedido explícito e errado numa
|
|
110
|
+
-- resposta silenciosa sobre outra conta.
|
|
111
|
+
RETURN NULL;
|
|
112
|
+
END IF;
|
|
113
|
+
|
|
114
|
+
-- ③ o recuo: conta única. Quem tem duas e não disse qual continua sem tenant,
|
|
115
|
+
-- que é a resposta correta — só que agora ela é evitável.
|
|
116
|
+
SELECT count(*), (array_agg(m.tenant_id))[1] INTO v_n, v_tenant
|
|
117
|
+
FROM app.memberships m WHERE m.user_id = v_uid AND m.active;
|
|
118
|
+
IF v_n = 1 THEN
|
|
119
|
+
RETURN v_tenant;
|
|
120
|
+
END IF;
|
|
121
|
+
RETURN NULL;
|
|
122
|
+
END $function$
|
|
123
|
+
|
|
124
|
+
;
|
|
125
|
+
CREATE OR REPLACE FUNCTION app._current_unit_id_raw()
|
|
126
|
+
RETURNS uuid
|
|
127
|
+
LANGUAGE plpgsql
|
|
128
|
+
STABLE SECURITY DEFINER
|
|
129
|
+
SET search_path TO ''
|
|
130
|
+
AS $function$
|
|
131
|
+
DECLARE
|
|
132
|
+
v_jwt jsonb := coalesce(auth.jwt(), '{}'::jsonb);
|
|
133
|
+
v_raw text;
|
|
134
|
+
v_unit uuid;
|
|
135
|
+
v_uid uuid;
|
|
136
|
+
BEGIN
|
|
137
|
+
-- ① a claim assinada, quando existir. Inalterado.
|
|
138
|
+
IF jsonb_exists(v_jwt, 'unit_id') THEN
|
|
139
|
+
v_raw := v_jwt ->> 'unit_id';
|
|
140
|
+
IF v_raw IS NULL OR v_raw !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' THEN
|
|
141
|
+
RETURN '00000000-0000-0000-0000-000000000000'::uuid;
|
|
142
|
+
END IF;
|
|
143
|
+
RETURN v_raw::uuid;
|
|
144
|
+
END IF;
|
|
145
|
+
|
|
146
|
+
-- ② o cabeçalho do app. Ele ESCOLHE entre as unidades que a pessoa já
|
|
147
|
+
-- alcança; não concede nenhuma.
|
|
148
|
+
v_unit := app.requested_unit();
|
|
149
|
+
IF v_unit IS NULL THEN
|
|
150
|
+
RETURN NULL; -- "todas as unidades" — o seletor está em rede
|
|
151
|
+
END IF;
|
|
152
|
+
|
|
153
|
+
v_uid := auth.uid();
|
|
154
|
+
IF v_uid IS NULL THEN
|
|
155
|
+
RETURN '00000000-0000-0000-0000-000000000000'::uuid;
|
|
156
|
+
END IF;
|
|
157
|
+
|
|
158
|
+
IF EXISTS (SELECT 1 FROM app.user_unit_access a
|
|
159
|
+
WHERE a.tenant_id = app.current_tenant_id()
|
|
160
|
+
AND a.user_id = v_uid
|
|
161
|
+
AND a.unit_id = v_unit) THEN
|
|
162
|
+
RETURN v_unit;
|
|
163
|
+
END IF;
|
|
164
|
+
|
|
165
|
+
-- Pediu uma unidade que não é sua: nega. O sentinela não casa com unidade
|
|
166
|
+
-- nenhuma, então a tela vem vazia — que é a resposta certa, e muito melhor
|
|
167
|
+
-- que devolver a rede inteira.
|
|
168
|
+
RETURN '00000000-0000-0000-0000-000000000000'::uuid;
|
|
169
|
+
END $function$
|
|
170
|
+
|
|
171
|
+
;
|
|
172
|
+
CREATE OR REPLACE FUNCTION app._unit_scoping_on_raw()
|
|
173
|
+
RETURNS boolean
|
|
174
|
+
LANGUAGE sql
|
|
175
|
+
STABLE SECURITY DEFINER
|
|
176
|
+
SET search_path TO ''
|
|
177
|
+
AS $function$
|
|
178
|
+
SELECT coalesce((SELECT t.unit_scoping_enabled FROM public.tenants t
|
|
179
|
+
WHERE t.id = app.current_tenant_id()), false);
|
|
180
|
+
$function$
|
|
181
|
+
|
|
182
|
+
;
|
|
183
|
+
|
|
184
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
-- 2. Os invólucros que memoizam
|
|
186
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
CREATE OR REPLACE FUNCTION app.current_tenant_id()
|
|
189
|
+
RETURNS uuid
|
|
190
|
+
LANGUAGE plpgsql
|
|
191
|
+
STABLE SECURITY DEFINER
|
|
192
|
+
SET search_path TO ''
|
|
193
|
+
AS $function$
|
|
194
|
+
DECLARE v_cache text := current_setting('app.ctx_tenant', true); v_out uuid;
|
|
195
|
+
BEGIN
|
|
196
|
+
IF v_cache IS NOT NULL AND v_cache <> '' THEN
|
|
197
|
+
RETURN nullif(v_cache, '-')::uuid;
|
|
198
|
+
END IF;
|
|
199
|
+
v_out := app._current_tenant_id_raw();
|
|
200
|
+
PERFORM set_config('app.ctx_tenant', coalesce(v_out::text, '-'), true);
|
|
201
|
+
RETURN v_out;
|
|
202
|
+
END $function$;
|
|
203
|
+
|
|
204
|
+
CREATE OR REPLACE FUNCTION app.current_unit_id()
|
|
205
|
+
RETURNS uuid
|
|
206
|
+
LANGUAGE plpgsql
|
|
207
|
+
STABLE SECURITY DEFINER
|
|
208
|
+
SET search_path TO ''
|
|
209
|
+
AS $function$
|
|
210
|
+
DECLARE v_cache text := current_setting('app.ctx_unit', true); v_out uuid;
|
|
211
|
+
BEGIN
|
|
212
|
+
IF v_cache IS NOT NULL AND v_cache <> '' THEN
|
|
213
|
+
RETURN nullif(v_cache, '-')::uuid;
|
|
214
|
+
END IF;
|
|
215
|
+
v_out := app._current_unit_id_raw();
|
|
216
|
+
PERFORM set_config('app.ctx_unit', coalesce(v_out::text, '-'), true);
|
|
217
|
+
RETURN v_out;
|
|
218
|
+
END $function$;
|
|
219
|
+
|
|
220
|
+
CREATE OR REPLACE FUNCTION app.unit_scoping_on()
|
|
221
|
+
RETURNS boolean
|
|
222
|
+
LANGUAGE plpgsql
|
|
223
|
+
STABLE SECURITY DEFINER
|
|
224
|
+
SET search_path TO ''
|
|
225
|
+
AS $function$
|
|
226
|
+
DECLARE v_cache text := current_setting('app.ctx_unit_scoping', true); v_out boolean;
|
|
227
|
+
BEGIN
|
|
228
|
+
IF v_cache IS NOT NULL AND v_cache <> '' THEN
|
|
229
|
+
RETURN v_cache = 't';
|
|
230
|
+
END IF;
|
|
231
|
+
v_out := app._unit_scoping_on_raw();
|
|
232
|
+
PERFORM set_config('app.ctx_unit_scoping', CASE WHEN v_out THEN 't' ELSE 'f' END, true);
|
|
233
|
+
RETURN v_out;
|
|
234
|
+
END $function$;
|
|
235
|
+
|
|
236
|
+
COMMENT ON FUNCTION app.current_tenant_id() IS
|
|
237
|
+
'O tenant da requisição, calculado UMA vez por transação. A regra mora em '
|
|
238
|
+
'app._current_tenant_id_raw() e não mudou; o que muda é quantas vezes ela '
|
|
239
|
+
'roda. Sem isto, contar um mês de agenda custava 2,7 ms por linha e os '
|
|
240
|
+
'painéis estouravam o relógio.';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
-- A permissão por unidade se resolve uma vez por unidade, não por linha.
|
|
2
|
+
--
|
|
3
|
+
-- Depois da 076 o contexto da requisição já não é recalculado por linha, e
|
|
4
|
+
-- contar um mês de agenda caiu de 21,9 s para 11,8 s. Ainda é muito: 1,47 ms
|
|
5
|
+
-- por linha, e a maior parte do que sobrou é `app.has_permission`.
|
|
6
|
+
--
|
|
7
|
+
-- app.has_permission(chave, unidade) 1,002 ms por chamada
|
|
8
|
+
--
|
|
9
|
+
-- Ela não pôde ser memoizada na 076 porque RECEBE ARGUMENTOS — e é a política
|
|
10
|
+
-- que os fornece, com o `unit_id` da linha:
|
|
11
|
+
--
|
|
12
|
+
-- CASE WHEN unit_id IS NULL THEN app.has_permission('agenda.read', NULL)
|
|
13
|
+
-- ELSE app.has_permission('agenda.read', unit_id) END
|
|
14
|
+
--
|
|
15
|
+
-- Só que os argumentos distintos dentro de UMA consulta são poucos: uma
|
|
16
|
+
-- permissão, e as unidades que aparecem na página. Oito mil linhas produzem
|
|
17
|
+
-- talvez três respostas diferentes — e a função as recalculava oito mil vezes,
|
|
18
|
+
-- cada uma passando por `app.effective_role_id`, que busca o papel efetivo do
|
|
19
|
+
-- usuário naquela unidade.
|
|
20
|
+
--
|
|
21
|
+
-- ── O cache com chave ───────────────────────────────────────────────────────
|
|
22
|
+
--
|
|
23
|
+
-- Um GUC transacional guardando um MAPA jsonb, com chave
|
|
24
|
+
-- `<permissão>|<unidade>`. Ler e escrever um mapa pequeno custa microssegundos
|
|
25
|
+
-- contra o milissegundo da resolução de papel.
|
|
26
|
+
--
|
|
27
|
+
-- O corpo da regra fica intocado em `app._has_permission_raw()`, como na 076 e
|
|
28
|
+
-- pelo mesmo motivo: isto decide quem vê o quê, e uma otimização não é hora de
|
|
29
|
+
-- mexer em regra de acesso. Muda quantas vezes ela roda.
|
|
30
|
+
--
|
|
31
|
+
-- Transacional, então morre com a requisição — o papel de alguém pode mudar a
|
|
32
|
+
-- qualquer momento e a próxima requisição já vê o novo. Dentro de uma
|
|
33
|
+
-- transação ele não muda, e é só isso que o cache assume.
|
|
34
|
+
--
|
|
35
|
+
-- Idempotente.
|
|
36
|
+
|
|
37
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
-- 1. A regra, intocada
|
|
39
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
CREATE OR REPLACE FUNCTION app._has_permission_raw(p_perm text, p_unit uuid DEFAULT NULL::uuid)
|
|
42
|
+
RETURNS boolean
|
|
43
|
+
LANGUAGE sql
|
|
44
|
+
STABLE SECURITY DEFINER
|
|
45
|
+
SET search_path TO ''
|
|
46
|
+
AS $function$
|
|
47
|
+
SELECT EXISTS (
|
|
48
|
+
SELECT 1
|
|
49
|
+
FROM app.role_permissions rp
|
|
50
|
+
WHERE rp.tenant_id = app.current_tenant_id()
|
|
51
|
+
AND rp.role_id = app.effective_role_id(app.current_tenant_id(), auth.uid(), p_unit)
|
|
52
|
+
AND rp.permission IN (p_perm, regexp_replace(p_perm, '\.[^.]+$', '.manage'))
|
|
53
|
+
);
|
|
54
|
+
$function$;
|
|
55
|
+
|
|
56
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
-- 2. O invólucro com memória
|
|
58
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
CREATE OR REPLACE FUNCTION app.has_permission(p_perm text, p_unit uuid DEFAULT NULL::uuid)
|
|
61
|
+
RETURNS boolean
|
|
62
|
+
LANGUAGE plpgsql
|
|
63
|
+
STABLE SECURITY DEFINER
|
|
64
|
+
SET search_path TO ''
|
|
65
|
+
AS $function$
|
|
66
|
+
DECLARE
|
|
67
|
+
v_chave text := p_perm || '|' || coalesce(p_unit::text, '-');
|
|
68
|
+
v_mapa jsonb := coalesce(nullif(current_setting('app.ctx_perm', true), '')::jsonb, '{}'::jsonb);
|
|
69
|
+
v_out boolean;
|
|
70
|
+
BEGIN
|
|
71
|
+
IF v_mapa ? v_chave THEN
|
|
72
|
+
RETURN (v_mapa ->> v_chave)::boolean;
|
|
73
|
+
END IF;
|
|
74
|
+
v_out := app._has_permission_raw(p_perm, p_unit);
|
|
75
|
+
PERFORM set_config('app.ctx_perm',
|
|
76
|
+
(v_mapa || jsonb_build_object(v_chave, v_out))::text, true);
|
|
77
|
+
RETURN v_out;
|
|
78
|
+
EXCEPTION
|
|
79
|
+
-- Um cache corrompido não pode virar negação de acesso: se o mapa não
|
|
80
|
+
-- parsear por qualquer motivo, responde a pergunta do jeito caro.
|
|
81
|
+
WHEN OTHERS THEN RETURN app._has_permission_raw(p_perm, p_unit);
|
|
82
|
+
END $function$;
|
|
83
|
+
|
|
84
|
+
COMMENT ON FUNCTION app.has_permission(text, uuid) IS
|
|
85
|
+
'A permissão do usuário, memoizada por (permissão, unidade) dentro da '
|
|
86
|
+
'transação. A regra mora em app._has_permission_raw() e não mudou. Sem o '
|
|
87
|
+
'cache ela custava 1 ms por LINHA lida — oito mil linhas de agenda para '
|
|
88
|
+
'três respostas distintas.';
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
-- A cerca também se lembra.
|
|
2
|
+
--
|
|
3
|
+
-- As 076 e 077 puseram memória no contexto da requisição e na permissão. O
|
|
4
|
+
-- caminho da agenda caiu de 21,9 s para 7,6 s num mês — mas ainda são 0,95 ms
|
|
5
|
+
-- por linha, e todo o resto está nas duas funções que faltaram:
|
|
6
|
+
--
|
|
7
|
+
-- app.has_unit(unidade) 0,445 ms por chamada, POR LINHA
|
|
8
|
+
-- app.owner_scoped(tipo) 0,408 ms
|
|
9
|
+
--
|
|
10
|
+
-- As duas recebem argumento, e por isso não entraram na 076. Mas os argumentos
|
|
11
|
+
-- distintos dentro de uma consulta são poucos — um tipo de recurso, e as
|
|
12
|
+
-- unidades que aparecem na página. `has_unit` responde no máximo 69 coisas
|
|
13
|
+
-- diferentes neste tenant; ela era chamada oito mil vezes.
|
|
14
|
+
--
|
|
15
|
+
-- Mesma forma das anteriores: a regra fica intocada em `app._*_raw()`, e o
|
|
16
|
+
-- invólucro guarda um mapa jsonb num GUC transacional. As duas dividem um mapa só (`app.ctx_acl`), porque respondem à mesma
|
|
17
|
+
-- pergunta de fundo — o que este usuário alcança — e um mapa é mais barato de
|
|
18
|
+
-- ler que dois.
|
|
19
|
+
--
|
|
20
|
+
-- `grants_exist` fica de fora de propósito: os planos mostram que ela já é
|
|
21
|
+
-- avaliada como InitPlan, uma vez por consulta, porque seus argumentos são
|
|
22
|
+
-- constantes. Memoizá-la seria cache sobre cache.
|
|
23
|
+
--
|
|
24
|
+
-- Idempotente.
|
|
25
|
+
|
|
26
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
-- 1. As regras, intocadas
|
|
28
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
CREATE OR REPLACE FUNCTION app._has_unit_raw(p_unit uuid)
|
|
31
|
+
RETURNS boolean
|
|
32
|
+
LANGUAGE sql
|
|
33
|
+
STABLE SECURITY DEFINER
|
|
34
|
+
SET search_path TO ''
|
|
35
|
+
AS $function$
|
|
36
|
+
SELECT p_unit IS NOT NULL
|
|
37
|
+
AND (app.current_unit_id() IS NULL OR p_unit = app.current_unit_id())
|
|
38
|
+
AND CASE WHEN app.unit_scoping_on()
|
|
39
|
+
THEN EXISTS (SELECT 1 FROM app.user_unit_access a
|
|
40
|
+
WHERE a.tenant_id = app.current_tenant_id()
|
|
41
|
+
AND a.user_id = auth.uid()
|
|
42
|
+
AND a.unit_id = p_unit)
|
|
43
|
+
ELSE EXISTS (SELECT 1 FROM app.units u
|
|
44
|
+
WHERE u.id = p_unit AND u.tenant_id = app.current_tenant_id())
|
|
45
|
+
END;
|
|
46
|
+
$function$
|
|
47
|
+
|
|
48
|
+
;
|
|
49
|
+
CREATE OR REPLACE FUNCTION app._owner_scoped_raw(p_resource_type text)
|
|
50
|
+
RETURNS boolean
|
|
51
|
+
LANGUAGE sql
|
|
52
|
+
STABLE SECURITY DEFINER
|
|
53
|
+
SET search_path TO ''
|
|
54
|
+
AS $function$
|
|
55
|
+
SELECT coalesce(
|
|
56
|
+
(SELECT v.owner_scoped FROM app.vertical_owner_scoping v
|
|
57
|
+
JOIN public.tenants t ON t.role_template = v.role_template
|
|
58
|
+
WHERE t.id = app.current_tenant_id() AND v.resource_type = p_resource_type),
|
|
59
|
+
-- no configuration for this vertical: the table's own flag, which defaults
|
|
60
|
+
-- true. Configuration relaxes; it never tightens by omission.
|
|
61
|
+
(SELECT r.owner_scoping FROM app.scaffold_registry r WHERE r.resource_type = p_resource_type LIMIT 1),
|
|
62
|
+
true)
|
|
63
|
+
$function$
|
|
64
|
+
|
|
65
|
+
;
|
|
66
|
+
|
|
67
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
-- 2. Os invólucros com memória
|
|
69
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
CREATE OR REPLACE FUNCTION app.has_unit(p_unit uuid)
|
|
72
|
+
RETURNS boolean
|
|
73
|
+
LANGUAGE plpgsql
|
|
74
|
+
STABLE SECURITY DEFINER
|
|
75
|
+
SET search_path TO ''
|
|
76
|
+
AS $function$
|
|
77
|
+
DECLARE
|
|
78
|
+
v_chave text := 'u|' || coalesce(p_unit::text, '-');
|
|
79
|
+
v_mapa jsonb := coalesce(nullif(current_setting('app.ctx_acl', true), '')::jsonb, '{}'::jsonb);
|
|
80
|
+
v_out boolean;
|
|
81
|
+
BEGIN
|
|
82
|
+
IF v_mapa ? v_chave THEN RETURN (v_mapa ->> v_chave)::boolean; END IF;
|
|
83
|
+
v_out := app._has_unit_raw(p_unit);
|
|
84
|
+
PERFORM set_config('app.ctx_acl', (v_mapa || jsonb_build_object(v_chave, v_out))::text, true);
|
|
85
|
+
RETURN v_out;
|
|
86
|
+
EXCEPTION WHEN OTHERS THEN RETURN app._has_unit_raw(p_unit);
|
|
87
|
+
END $function$;
|
|
88
|
+
|
|
89
|
+
CREATE OR REPLACE FUNCTION app.owner_scoped(p_resource_type text)
|
|
90
|
+
RETURNS boolean
|
|
91
|
+
LANGUAGE plpgsql
|
|
92
|
+
STABLE SECURITY DEFINER
|
|
93
|
+
SET search_path TO ''
|
|
94
|
+
AS $function$
|
|
95
|
+
DECLARE
|
|
96
|
+
v_chave text := 'o|' || coalesce(p_resource_type, '-');
|
|
97
|
+
v_mapa jsonb := coalesce(nullif(current_setting('app.ctx_acl', true), '')::jsonb, '{}'::jsonb);
|
|
98
|
+
v_out boolean;
|
|
99
|
+
BEGIN
|
|
100
|
+
IF v_mapa ? v_chave THEN RETURN (v_mapa ->> v_chave)::boolean; END IF;
|
|
101
|
+
v_out := app._owner_scoped_raw(p_resource_type);
|
|
102
|
+
PERFORM set_config('app.ctx_acl', (v_mapa || jsonb_build_object(v_chave, v_out))::text, true);
|
|
103
|
+
RETURN v_out;
|
|
104
|
+
EXCEPTION WHEN OTHERS THEN RETURN app._owner_scoped_raw(p_resource_type);
|
|
105
|
+
END $function$;
|
|
106
|
+
|
|
107
|
+
COMMENT ON FUNCTION app.has_unit(uuid) IS
|
|
108
|
+
'A cerca da unidade, memoizada por unidade dentro da transação. A regra mora '
|
|
109
|
+
'em app._has_unit_raw() e não mudou. Ela responde no máximo uma coisa por '
|
|
110
|
+
'unidade da rede, e era chamada uma vez por linha lida.';
|