@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,327 @@
|
|
|
1
|
+
-- O cache do contexto sabe de quem ele é
|
|
2
|
+
--
|
|
3
|
+
-- A 076 passou a calcular o contexto da requisição UMA vez por transação, e
|
|
4
|
+
-- está certa no motivo: contar um mês de agenda pagava 2,7 ms por linha para
|
|
5
|
+
-- reconfirmar a mesma resposta. O que ela não fez foi dizer de QUEM é a
|
|
6
|
+
-- resposta guardada.
|
|
7
|
+
--
|
|
8
|
+
-- DECLARE v_cache text := current_setting('app.ctx_tenant', true);
|
|
9
|
+
-- IF v_cache IS NOT NULL AND v_cache <> '' THEN
|
|
10
|
+
-- RETURN nullif(v_cache, '-')::uuid; -- <- de quem?
|
|
11
|
+
--
|
|
12
|
+
-- `set_config(..., true)` é local à TRANSAÇÃO, não à requisição. Enquanto a
|
|
13
|
+
-- claim não muda dentro da transação, as duas coisas coincidem e ninguém nota.
|
|
14
|
+
-- Quando ela muda, o cache responde pelo dono anterior.
|
|
15
|
+
--
|
|
16
|
+
-- ── Onde isso aparece ───────────────────────────────────────────────────────
|
|
17
|
+
--
|
|
18
|
+
-- O pgTAP troca de persona dentro de uma transação só — é o jeito de provar
|
|
19
|
+
-- justamente as regras que esta função implementa. Trinta e sete suítes
|
|
20
|
+
-- quebraram de uma vez, todas na mesma frase:
|
|
21
|
+
--
|
|
22
|
+
-- "a claim naming another tenant resolves to no tenant"
|
|
23
|
+
-- have: 3fc8bcfd-… (o salão, do teste anterior)
|
|
24
|
+
-- want: NULL
|
|
25
|
+
--
|
|
26
|
+
-- O teste não estava errado. A função é que respondia pela persona passada.
|
|
27
|
+
--
|
|
28
|
+
-- Em produção o PostgREST abre uma transação por requisição e a claim não muda
|
|
29
|
+
-- no meio, então o furo não aparece — mas ele existe para qualquer caminho que
|
|
30
|
+
-- troque de sujeito dentro de uma transação, e o primeiro que fizer isso vai
|
|
31
|
+
-- receber a resposta de outra pessoa sem nenhum aviso.
|
|
32
|
+
--
|
|
33
|
+
-- ── O conserto ──────────────────────────────────────────────────────────────
|
|
34
|
+
--
|
|
35
|
+
-- Guardar, junto do valor, a CHAVE de quem o produziu, e só aceitar o cache
|
|
36
|
+
-- quando a chave é a mesma. A chave é a própria claim: `auth.uid()` e
|
|
37
|
+
-- `auth.jwt()` saem ambos de `request.jwt.claims`, então ela cobre todas as
|
|
38
|
+
-- entradas da regra crua. Para a unidade entra também o cabeçalho, que é de
|
|
39
|
+
-- onde `x-fayz-unit` vem.
|
|
40
|
+
--
|
|
41
|
+
-- O ganho da 076 fica de pé: dentro de uma requisição real a chave não muda, o
|
|
42
|
+
-- cache acerta toda vez, e a regra crua continua rodando uma vez só. O que
|
|
43
|
+
-- muda é que agora ele sabe quando não é dele que estão falando.
|
|
44
|
+
--
|
|
45
|
+
-- A regra em si não é tocada: `app._current_tenant_id_raw()` continua igual.
|
|
46
|
+
-- Idempotente.
|
|
47
|
+
|
|
48
|
+
CREATE OR REPLACE FUNCTION app.current_tenant_id()
|
|
49
|
+
RETURNS uuid
|
|
50
|
+
LANGUAGE plpgsql
|
|
51
|
+
STABLE SECURITY DEFINER
|
|
52
|
+
SET search_path TO ''
|
|
53
|
+
AS $function$
|
|
54
|
+
DECLARE
|
|
55
|
+
-- As DUAS grafias: `auth.uid()` le `request.jwt.claim.sub` e, se nao houver,
|
|
56
|
+
-- o `sub` de dentro de `request.jwt.claims`. Uma chave que olhasse so a
|
|
57
|
+
-- segunda nao veria a troca de pessoa feita pela primeira — que e como o
|
|
58
|
+
-- pgTAP e o codigo mais antigo trocam de sujeito.
|
|
59
|
+
v_key text := coalesce(current_setting('request.jwt.claims', true), '')
|
|
60
|
+
|| '|' || coalesce(current_setting('request.jwt.claim.sub', true), '');
|
|
61
|
+
v_cache text := current_setting('app.ctx_tenant', true);
|
|
62
|
+
v_owner text := current_setting('app.ctx_tenant_key', true);
|
|
63
|
+
v_out uuid;
|
|
64
|
+
BEGIN
|
|
65
|
+
IF v_cache IS NOT NULL AND v_cache <> '' AND v_owner IS NOT DISTINCT FROM v_key THEN
|
|
66
|
+
RETURN nullif(v_cache, '-')::uuid;
|
|
67
|
+
END IF;
|
|
68
|
+
v_out := app._current_tenant_id_raw();
|
|
69
|
+
PERFORM set_config('app.ctx_tenant', coalesce(v_out::text, '-'), true);
|
|
70
|
+
PERFORM set_config('app.ctx_tenant_key', v_key, true);
|
|
71
|
+
RETURN v_out;
|
|
72
|
+
END $function$;
|
|
73
|
+
|
|
74
|
+
CREATE OR REPLACE FUNCTION app.current_unit_id()
|
|
75
|
+
RETURNS uuid
|
|
76
|
+
LANGUAGE plpgsql
|
|
77
|
+
STABLE SECURITY DEFINER
|
|
78
|
+
SET search_path TO ''
|
|
79
|
+
AS $function$
|
|
80
|
+
DECLARE
|
|
81
|
+
-- A unidade depende de quem pergunta E do cabeçalho que ele mandou.
|
|
82
|
+
v_key text := coalesce(current_setting('request.jwt.claims', true), '')
|
|
83
|
+
|| '|' || coalesce(current_setting('request.jwt.claim.sub', true), '')
|
|
84
|
+
|| '|' || coalesce(current_setting('request.headers', true), '');
|
|
85
|
+
v_cache text := current_setting('app.ctx_unit', true);
|
|
86
|
+
v_owner text := current_setting('app.ctx_unit_key', true);
|
|
87
|
+
v_out uuid;
|
|
88
|
+
BEGIN
|
|
89
|
+
IF v_cache IS NOT NULL AND v_cache <> '' AND v_owner IS NOT DISTINCT FROM v_key THEN
|
|
90
|
+
RETURN nullif(v_cache, '-')::uuid;
|
|
91
|
+
END IF;
|
|
92
|
+
v_out := app._current_unit_id_raw();
|
|
93
|
+
PERFORM set_config('app.ctx_unit', coalesce(v_out::text, '-'), true);
|
|
94
|
+
PERFORM set_config('app.ctx_unit_key', v_key, true);
|
|
95
|
+
RETURN v_out;
|
|
96
|
+
END $function$;
|
|
97
|
+
|
|
98
|
+
CREATE OR REPLACE FUNCTION app.unit_scoping_on()
|
|
99
|
+
RETURNS boolean
|
|
100
|
+
LANGUAGE plpgsql
|
|
101
|
+
STABLE SECURITY DEFINER
|
|
102
|
+
SET search_path TO ''
|
|
103
|
+
AS $function$
|
|
104
|
+
DECLARE
|
|
105
|
+
v_key text := coalesce(current_setting('request.jwt.claims', true), '')
|
|
106
|
+
|| '|' || coalesce(current_setting('request.jwt.claim.sub', true), '');
|
|
107
|
+
v_cache text := current_setting('app.ctx_unit_scoping', true);
|
|
108
|
+
v_owner text := current_setting('app.ctx_unit_scoping_key', true);
|
|
109
|
+
v_out boolean;
|
|
110
|
+
BEGIN
|
|
111
|
+
IF v_cache IS NOT NULL AND v_cache <> '' AND v_owner IS NOT DISTINCT FROM v_key THEN
|
|
112
|
+
RETURN v_cache = 't';
|
|
113
|
+
END IF;
|
|
114
|
+
v_out := app._unit_scoping_on_raw();
|
|
115
|
+
PERFORM set_config('app.ctx_unit_scoping', CASE WHEN v_out THEN 't' ELSE 'f' END, true);
|
|
116
|
+
PERFORM set_config('app.ctx_unit_scoping_key', v_key, true);
|
|
117
|
+
RETURN v_out;
|
|
118
|
+
END $function$;
|
|
119
|
+
|
|
120
|
+
COMMENT ON FUNCTION app.current_tenant_id() IS
|
|
121
|
+
'O tenant da requisição, calculado UMA vez por transação POR CLAIM. A regra '
|
|
122
|
+
'mora em app._current_tenant_id_raw() e não mudou. O cache da 076 era local '
|
|
123
|
+
'à transação sem dizer de quem era a resposta: trocar de sujeito dentro da '
|
|
124
|
+
'mesma transação devolvia o tenant do sujeito anterior.';
|
|
125
|
+
|
|
126
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
-- Os outros dois mapas da mesma onda
|
|
128
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
+
--
|
|
130
|
+
-- A 077 (`app.ctx_perm`) e a 078 (`app.ctx_acl`) guardam MAPAS, e não um valor
|
|
131
|
+
-- só — mas o defeito é idêntico: a chave descreve a pergunta e não quem a fez.
|
|
132
|
+
--
|
|
133
|
+
-- v_chave text := p_perm || '|' || coalesce(p_unit::text, '-');
|
|
134
|
+
--
|
|
135
|
+
-- Duas personas na mesma transação perguntam "people.read" e a segunda recebe
|
|
136
|
+
-- a resposta da primeira. Aqui isso aparecia ao contrário do caso do tenant, e
|
|
137
|
+
-- por isso é mais perigoso: a recepcionista da clínica perdia uma permissão
|
|
138
|
+
-- que tem, e o teste do proprietário ganhava uma que não é dele.
|
|
139
|
+
--
|
|
140
|
+
-- O conserto é o mesmo: a chave passa a dizer de quem é a resposta. Entradas
|
|
141
|
+
-- de outra persona simplesmente não casam. O mapa é local à transação, então
|
|
142
|
+
-- carregar algumas chaves a mais não tem custo que se meça.
|
|
143
|
+
|
|
144
|
+
CREATE OR REPLACE FUNCTION app.ctx_owner_key()
|
|
145
|
+
RETURNS text
|
|
146
|
+
LANGUAGE sql
|
|
147
|
+
STABLE
|
|
148
|
+
SET search_path TO ''
|
|
149
|
+
AS $function$
|
|
150
|
+
SELECT coalesce(current_setting('request.jwt.claims', true), '')
|
|
151
|
+
|| '|' || coalesce(current_setting('request.jwt.claim.sub', true), '')
|
|
152
|
+
$function$;
|
|
153
|
+
|
|
154
|
+
COMMENT ON FUNCTION app.ctx_owner_key() IS
|
|
155
|
+
'De quem é a resposta guardada nos caches de contexto (076/077/078). A claim '
|
|
156
|
+
'inteira, porque auth.uid() e auth.jwt() saem dela — então ela cobre todas as '
|
|
157
|
+
'entradas das regras cruas.';
|
|
158
|
+
|
|
159
|
+
CREATE OR REPLACE FUNCTION app.has_permission(p_perm text, p_unit uuid DEFAULT NULL::uuid)
|
|
160
|
+
RETURNS boolean
|
|
161
|
+
LANGUAGE plpgsql
|
|
162
|
+
STABLE SECURITY DEFINER
|
|
163
|
+
SET search_path TO ''
|
|
164
|
+
AS $function$
|
|
165
|
+
DECLARE
|
|
166
|
+
v_chave text := app.ctx_owner_key() || '|' || p_perm || '|' || coalesce(p_unit::text, '-');
|
|
167
|
+
v_mapa jsonb := coalesce(nullif(current_setting('app.ctx_perm', true), '')::jsonb, '{}'::jsonb);
|
|
168
|
+
v_out boolean;
|
|
169
|
+
BEGIN
|
|
170
|
+
IF v_mapa ? v_chave THEN
|
|
171
|
+
RETURN (v_mapa ->> v_chave)::boolean;
|
|
172
|
+
END IF;
|
|
173
|
+
v_out := app._has_permission_raw(p_perm, p_unit);
|
|
174
|
+
PERFORM set_config('app.ctx_perm',
|
|
175
|
+
(v_mapa || jsonb_build_object(v_chave, v_out))::text, true);
|
|
176
|
+
RETURN v_out;
|
|
177
|
+
EXCEPTION
|
|
178
|
+
-- Um cache corrompido não pode virar negação de acesso: se o mapa não
|
|
179
|
+
-- parsear por qualquer motivo, responde a pergunta do jeito caro.
|
|
180
|
+
WHEN OTHERS THEN RETURN app._has_permission_raw(p_perm, p_unit);
|
|
181
|
+
END $function$;
|
|
182
|
+
|
|
183
|
+
CREATE OR REPLACE FUNCTION app.has_unit(p_unit uuid)
|
|
184
|
+
RETURNS boolean
|
|
185
|
+
LANGUAGE plpgsql
|
|
186
|
+
STABLE SECURITY DEFINER
|
|
187
|
+
SET search_path TO ''
|
|
188
|
+
AS $function$
|
|
189
|
+
DECLARE
|
|
190
|
+
v_chave text := app.ctx_owner_key() || '|u|' || coalesce(p_unit::text, '-');
|
|
191
|
+
v_mapa jsonb := coalesce(nullif(current_setting('app.ctx_acl', true), '')::jsonb, '{}'::jsonb);
|
|
192
|
+
v_out boolean;
|
|
193
|
+
BEGIN
|
|
194
|
+
IF v_mapa ? v_chave THEN RETURN (v_mapa ->> v_chave)::boolean; END IF;
|
|
195
|
+
v_out := app._has_unit_raw(p_unit);
|
|
196
|
+
PERFORM set_config('app.ctx_acl', (v_mapa || jsonb_build_object(v_chave, v_out))::text, true);
|
|
197
|
+
RETURN v_out;
|
|
198
|
+
EXCEPTION WHEN OTHERS THEN RETURN app._has_unit_raw(p_unit);
|
|
199
|
+
END $function$;
|
|
200
|
+
|
|
201
|
+
CREATE OR REPLACE FUNCTION app.owner_scoped(p_resource_type text)
|
|
202
|
+
RETURNS boolean
|
|
203
|
+
LANGUAGE plpgsql
|
|
204
|
+
STABLE SECURITY DEFINER
|
|
205
|
+
SET search_path TO ''
|
|
206
|
+
AS $function$
|
|
207
|
+
DECLARE
|
|
208
|
+
v_chave text := app.ctx_owner_key() || '|o|' || coalesce(p_resource_type, '-');
|
|
209
|
+
v_mapa jsonb := coalesce(nullif(current_setting('app.ctx_acl', true), '')::jsonb, '{}'::jsonb);
|
|
210
|
+
v_out boolean;
|
|
211
|
+
BEGIN
|
|
212
|
+
IF v_mapa ? v_chave THEN RETURN (v_mapa ->> v_chave)::boolean; END IF;
|
|
213
|
+
v_out := app._owner_scoped_raw(p_resource_type);
|
|
214
|
+
PERFORM set_config('app.ctx_acl', (v_mapa || jsonb_build_object(v_chave, v_out))::text, true);
|
|
215
|
+
RETURN v_out;
|
|
216
|
+
EXCEPTION WHEN OTHERS THEN RETURN app._owner_scoped_raw(p_resource_type);
|
|
217
|
+
END $function$;
|
|
218
|
+
|
|
219
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
220
|
+
-- E as funções novas não nascem abertas
|
|
221
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
222
|
+
--
|
|
223
|
+
-- Toda função criada no Postgres nasce com EXECUTE para PUBLIC. A onda de
|
|
224
|
+
-- memoização (076, 077, 078) criou SEIS auxiliares `_raw` em `app` e não
|
|
225
|
+
-- revogou nenhuma; esta migration criou a sétima. O contrato N15c do
|
|
226
|
+
-- `900_verification` mede exatamente isso e contava oito:
|
|
227
|
+
--
|
|
228
|
+
-- anon can execute nothing callable in schema app have: 8 want: 0
|
|
229
|
+
--
|
|
230
|
+
-- Não é um detalhe de teste. `app._has_permission_raw('people.read')` é a
|
|
231
|
+
-- resposta CRUA da pergunta de permissão, sem cache e sem cerca; deixá-la
|
|
232
|
+
-- aberta a `anon` é deixar a pergunta de autorização atender quem não se
|
|
233
|
+
-- identificou.
|
|
234
|
+
--
|
|
235
|
+
-- As públicas (`current_tenant_id`, `has_permission`, `has_unit`,
|
|
236
|
+
-- `owner_scoped`, `unit_scoping_on`) NÃO são tocadas: elas já existiam, o
|
|
237
|
+
-- `CREATE OR REPLACE` preserva as concessões delas, e as políticas de RLS
|
|
238
|
+
-- dependem de `authenticated` poder chamá-las.
|
|
239
|
+
--
|
|
240
|
+
-- As `_raw` e a chave do cache só são chamadas de dentro de funções
|
|
241
|
+
-- SECURITY DEFINER, que rodam como o dono — então ninguém perde nada.
|
|
242
|
+
|
|
243
|
+
REVOKE ALL ON FUNCTION app._current_tenant_id_raw() FROM PUBLIC, anon, authenticated;
|
|
244
|
+
REVOKE ALL ON FUNCTION app._current_unit_id_raw() FROM PUBLIC, anon, authenticated;
|
|
245
|
+
REVOKE ALL ON FUNCTION app._unit_scoping_on_raw() FROM PUBLIC, anon, authenticated;
|
|
246
|
+
REVOKE ALL ON FUNCTION app._has_permission_raw(text, uuid) FROM PUBLIC, anon, authenticated;
|
|
247
|
+
REVOKE ALL ON FUNCTION app._has_unit_raw(uuid) FROM PUBLIC, anon, authenticated;
|
|
248
|
+
REVOKE ALL ON FUNCTION app._owner_scoped_raw(text) FROM PUBLIC, anon, authenticated;
|
|
249
|
+
REVOKE ALL ON FUNCTION app.ctx_owner_key() FROM PUBLIC, anon, authenticated;
|
|
250
|
+
|
|
251
|
+
-- As duas que faltavam: `requested_unit` mudou de schema na 076 e `brand_of`
|
|
252
|
+
-- nasceu na 089 — as duas em `app`, as duas abertas a PUBLIC por nascimento.
|
|
253
|
+
REVOKE ALL ON FUNCTION app.requested_unit() FROM PUBLIC, anon;
|
|
254
|
+
REVOKE ALL ON FUNCTION app.brand_of(uuid) FROM PUBLIC, anon;
|
|
255
|
+
|
|
256
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
257
|
+
-- E quem MUDA a configuração limpa o cache de quem a lê
|
|
258
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
259
|
+
--
|
|
260
|
+
-- Dizer de quem é a resposta resolve a troca de PESSOA. Falta a outra metade:
|
|
261
|
+
-- a mesma pessoa, na mesma transação, depois que a configuração mudou.
|
|
262
|
+
--
|
|
263
|
+
-- -- ela não alcança B2
|
|
264
|
+
-- INSERT INTO app.membership_units (...) VALUES (... 'caixa-b2' ...);
|
|
265
|
+
-- -- ...e uma linha de configuração depois, alcança
|
|
266
|
+
--
|
|
267
|
+
-- Esse é o contrato que três suítes medem, e é também o que qualquer RPC que
|
|
268
|
+
-- concede acesso e responde em seguida depende. O cache das 076/077/078 é
|
|
269
|
+
-- local à transação: a escrita acontece, e a leitura seguinte devolve a
|
|
270
|
+
-- resposta de antes da escrita. Fica pior que sem cache, porque a função
|
|
271
|
+
-- passa a contradizer uma linha que a própria transação acabou de gravar.
|
|
272
|
+
--
|
|
273
|
+
-- A invalidação não pode ser por tempo nem por versão lida a cada chamada —
|
|
274
|
+
-- as duas custariam o que a memoização economizou. Ela é por EVENTO: quem
|
|
275
|
+
-- escreve na configuração apaga o cache, na mesma transação, de graça.
|
|
276
|
+
--
|
|
277
|
+
-- Gatilho de comando (e não de linha): uma importação de mil vínculos limpa o
|
|
278
|
+
-- cache uma vez, não mil.
|
|
279
|
+
|
|
280
|
+
CREATE OR REPLACE FUNCTION app.ctx_cache_reset()
|
|
281
|
+
RETURNS trigger
|
|
282
|
+
LANGUAGE plpgsql
|
|
283
|
+
SECURITY DEFINER
|
|
284
|
+
SET search_path TO ''
|
|
285
|
+
AS $function$
|
|
286
|
+
BEGIN
|
|
287
|
+
PERFORM set_config('app.ctx_tenant', '', true);
|
|
288
|
+
PERFORM set_config('app.ctx_tenant_key', '', true);
|
|
289
|
+
PERFORM set_config('app.ctx_unit', '', true);
|
|
290
|
+
PERFORM set_config('app.ctx_unit_key', '', true);
|
|
291
|
+
PERFORM set_config('app.ctx_unit_scoping', '', true);
|
|
292
|
+
PERFORM set_config('app.ctx_unit_scoping_key', '', true);
|
|
293
|
+
PERFORM set_config('app.ctx_perm', '', true);
|
|
294
|
+
PERFORM set_config('app.ctx_acl', '', true);
|
|
295
|
+
RETURN NULL;
|
|
296
|
+
END $function$;
|
|
297
|
+
|
|
298
|
+
COMMENT ON FUNCTION app.ctx_cache_reset() IS
|
|
299
|
+
'Apaga os caches de contexto da transação. Ligado a quem ESCREVE a '
|
|
300
|
+
'configuração que eles resumem: sem isto, uma leitura logo depois de uma '
|
|
301
|
+
'escrita na mesma transação responde pelo estado anterior.';
|
|
302
|
+
|
|
303
|
+
REVOKE ALL ON FUNCTION app.ctx_cache_reset() FROM PUBLIC, anon, authenticated;
|
|
304
|
+
|
|
305
|
+
DO $$
|
|
306
|
+
DECLARE
|
|
307
|
+
t text;
|
|
308
|
+
alvo text[] := ARRAY[
|
|
309
|
+
'app.memberships', -- de que conta eu sou
|
|
310
|
+
'app.membership_units', -- e com que papel em cada unidade
|
|
311
|
+
'app.roles',
|
|
312
|
+
'app.role_permissions', -- o que o papel pode
|
|
313
|
+
'app.user_unit_access', -- que unidades eu alcanço
|
|
314
|
+
'app.resource_grants', -- e o que está liberado em cada uma
|
|
315
|
+
'app.units',
|
|
316
|
+
'app.vertical_owner_scoping', -- a cerca por ramo tambem e configuracao
|
|
317
|
+
'public.tenants' -- inclusive o interruptor unit_scoping_enabled
|
|
318
|
+
];
|
|
319
|
+
BEGIN
|
|
320
|
+
FOREACH t IN ARRAY alvo LOOP
|
|
321
|
+
IF to_regclass(t) IS NULL THEN CONTINUE; END IF;
|
|
322
|
+
EXECUTE format('DROP TRIGGER IF EXISTS ctx_cache_reset ON %s', t);
|
|
323
|
+
EXECUTE format(
|
|
324
|
+
'CREATE TRIGGER ctx_cache_reset AFTER INSERT OR UPDATE OR DELETE ON %s '
|
|
325
|
+
'FOR EACH STATEMENT EXECUTE FUNCTION app.ctx_cache_reset()', t);
|
|
326
|
+
END LOOP;
|
|
327
|
+
END $$;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
-- O ramo volta a saber que papéis a conta nasce tendo
|
|
2
|
+
--
|
|
3
|
+
-- A 093 fechou `tenants.vertical_id` em sete nomes. `create_tenant` derivava o
|
|
4
|
+
-- conjunto de papéis procurando um template com o MESMO nome do ramo:
|
|
5
|
+
--
|
|
6
|
+
-- CASE WHEN EXISTS (SELECT 1 FROM app.role_templates rt
|
|
7
|
+
-- WHERE rt.template = p_vertical_id)
|
|
8
|
+
-- THEN p_vertical_id ELSE 'generic' END
|
|
9
|
+
--
|
|
10
|
+
-- Os templates se chamam `salon`, `clinic`, `restaurant`, `generic` — o
|
|
11
|
+
-- vocabulário de antes. Nenhum se chama `beauty` nem `food`. Desde a 093,
|
|
12
|
+
-- portanto, TODA conta criada nasce com o template `generic`: sem os papéis do
|
|
13
|
+
-- ofício, com o rail certo e a equipe errada. É o mesmo buraco da 107, no
|
|
14
|
+
-- outro lado da criação da conta, e pela mesma causa — fechar um vocabulário
|
|
15
|
+
-- sem visitar quem o lia.
|
|
16
|
+
--
|
|
17
|
+
-- A saída é a mesma também: traduzir num lugar só, e não batizar os templates
|
|
18
|
+
-- de novo. `beauty` cobre salão e clínica (as duas rodam o StudioControl), e é
|
|
19
|
+
-- por isso que ela cai em `salon`: a clínica é a variante, e quem a quer pede
|
|
20
|
+
-- `p_template` explícito — que continua vencendo a derivação, como sempre.
|
|
21
|
+
--
|
|
22
|
+
-- Idempotente.
|
|
23
|
+
|
|
24
|
+
CREATE OR REPLACE FUNCTION app.role_template_for_vertical(p_vertical text)
|
|
25
|
+
RETURNS text
|
|
26
|
+
LANGUAGE sql
|
|
27
|
+
STABLE
|
|
28
|
+
SET search_path TO ''
|
|
29
|
+
AS $function$
|
|
30
|
+
SELECT coalesce(
|
|
31
|
+
(SELECT rt.template FROM app.role_templates rt
|
|
32
|
+
WHERE rt.template = CASE p_vertical
|
|
33
|
+
WHEN 'food' THEN 'restaurant'
|
|
34
|
+
WHEN 'beauty' THEN 'salon'
|
|
35
|
+
ELSE p_vertical
|
|
36
|
+
END
|
|
37
|
+
LIMIT 1),
|
|
38
|
+
'generic');
|
|
39
|
+
$function$;
|
|
40
|
+
|
|
41
|
+
COMMENT ON FUNCTION app.role_template_for_vertical(text) IS
|
|
42
|
+
'Que conjunto de papéis uma conta daquele ramo nasce tendo. Traduz o '
|
|
43
|
+
'vocabulário fechado de tenants.vertical_id (093) para os nomes dos '
|
|
44
|
+
'templates, que são anteriores a ele. Ramo sem template próprio cai em '
|
|
45
|
+
'generic — que é a resposta honesta, não um erro.';
|
|
46
|
+
|
|
47
|
+
REVOKE ALL ON FUNCTION app.role_template_for_vertical(text) FROM PUBLIC, anon;
|
|
48
|
+
|
|
49
|
+
-- E `create_tenant` passa a perguntar a ela. Nada mais muda no corpo: a
|
|
50
|
+
-- definição abaixo é a que estava no banco, com a tradução no lugar do CASE.
|
|
51
|
+
|
|
52
|
+
CREATE OR REPLACE FUNCTION public.create_tenant(p_name text, p_slug text, p_kind text DEFAULT 'business'::text, p_vertical_id text DEFAULT NULL::text, p_template text DEFAULT 'generic'::text, p_settings jsonb DEFAULT '{}'::jsonb, p_hq_name text DEFAULT NULL::text)
|
|
53
|
+
RETURNS jsonb
|
|
54
|
+
LANGUAGE plpgsql
|
|
55
|
+
SECURITY DEFINER
|
|
56
|
+
SET search_path TO ''
|
|
57
|
+
AS $function$
|
|
58
|
+
DECLARE
|
|
59
|
+
v_uid uuid := auth.uid();
|
|
60
|
+
v_tenant uuid;
|
|
61
|
+
v_unit uuid;
|
|
62
|
+
v_membership uuid;
|
|
63
|
+
v_owner_role uuid;
|
|
64
|
+
v_template text;
|
|
65
|
+
v_slug text;
|
|
66
|
+
v_name text;
|
|
67
|
+
v_seeded jsonb;
|
|
68
|
+
BEGIN
|
|
69
|
+
IF v_uid IS NULL THEN
|
|
70
|
+
RAISE EXCEPTION 'create_tenant: the actor is derived from the session and there is no session'
|
|
71
|
+
USING ERRCODE = '42501';
|
|
72
|
+
END IF;
|
|
73
|
+
IF p_kind IS NULL OR p_kind NOT IN ('network', 'business') THEN
|
|
74
|
+
RAISE EXCEPTION 'create_tenant: kind must be network or business (got %)', coalesce(p_kind, '<null>')
|
|
75
|
+
USING ERRCODE = '22023';
|
|
76
|
+
END IF;
|
|
77
|
+
v_name := nullif(btrim(coalesce(p_name, '')), '');
|
|
78
|
+
IF v_name IS NULL THEN
|
|
79
|
+
RAISE EXCEPTION 'create_tenant: name is required' USING ERRCODE = '22023';
|
|
80
|
+
END IF;
|
|
81
|
+
v_slug := lower(btrim(coalesce(p_slug, '')));
|
|
82
|
+
IF v_slug !~ '^[a-z0-9]+([._-][a-z0-9]+)*$' OR length(v_slug) > 100 THEN
|
|
83
|
+
RAISE EXCEPTION 'create_tenant: slug must be lowercase letters and digits separated by single . _ - (got %)', coalesce(nullif(v_slug, ''), '<empty>')
|
|
84
|
+
USING ERRCODE = '22023';
|
|
85
|
+
END IF;
|
|
86
|
+
IF EXISTS (SELECT 1 FROM public.tenants t WHERE t.slug = v_slug) THEN
|
|
87
|
+
RAISE EXCEPTION 'create_tenant: slug % is already taken', v_slug USING ERRCODE = '23505';
|
|
88
|
+
END IF;
|
|
89
|
+
|
|
90
|
+
-- Template: explicit, else the vertical's own template when one exists, else generic.
|
|
91
|
+
v_template := coalesce(p_template, app.role_template_for_vertical(p_vertical_id));
|
|
92
|
+
IF NOT EXISTS (SELECT 1 FROM app.role_templates rt WHERE rt.template = v_template) THEN
|
|
93
|
+
RAISE EXCEPTION 'create_tenant: unknown role template %', v_template USING ERRCODE = '22023';
|
|
94
|
+
END IF;
|
|
95
|
+
|
|
96
|
+
INSERT INTO public.tenants (name, slug, plan, vertical_id, kind, settings, role_template)
|
|
97
|
+
VALUES (v_name, v_slug, 'free', p_vertical_id, p_kind, coalesce(p_settings, '{}'::jsonb), v_template)
|
|
98
|
+
RETURNING id INTO v_tenant;
|
|
99
|
+
|
|
100
|
+
v_owner_role := app.seed_role_template(v_tenant, v_template);
|
|
101
|
+
|
|
102
|
+
INSERT INTO app.units (tenant_id, kind, name, slug, active)
|
|
103
|
+
VALUES (v_tenant, 'hq', coalesce(nullif(btrim(coalesce(p_hq_name, '')), ''), v_name), 'hq', true)
|
|
104
|
+
RETURNING id INTO v_unit;
|
|
105
|
+
|
|
106
|
+
-- The owner: role from the template (holds the whole catalog), every unit.
|
|
107
|
+
INSERT INTO app.memberships (tenant_id, user_id, role_id, all_units, active)
|
|
108
|
+
VALUES (v_tenant, v_uid, v_owner_role, true, true)
|
|
109
|
+
RETURNING id INTO v_membership;
|
|
110
|
+
|
|
111
|
+
-- Os plugins do ramo. Mesma transação de propósito: um tenant que existe sem
|
|
112
|
+
-- plugin nenhum abre uma casca vazia, e quem cria acha que perdeu o que
|
|
113
|
+
-- digitou. Sem vertical (ou vertical desconhecido) devolve enabled 0 e segue
|
|
114
|
+
-- — nascer sem ramo é legítimo, nascer pela metade não.
|
|
115
|
+
v_seeded := public.tenant_apply_vertical_defaults(v_tenant, p_vertical_id);
|
|
116
|
+
|
|
117
|
+
RETURN jsonb_build_object(
|
|
118
|
+
'tenant', (SELECT to_jsonb(t) FROM public.tenants t WHERE t.id = v_tenant),
|
|
119
|
+
'hq_unit', (SELECT to_jsonb(u) FROM app.units u WHERE u.id = v_unit),
|
|
120
|
+
'membership', (SELECT to_jsonb(m) FROM app.memberships m WHERE m.id = v_membership),
|
|
121
|
+
'plugins', v_seeded
|
|
122
|
+
);
|
|
123
|
+
END $function$
|
|
124
|
+
;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
-- A resposta inicial volta a dizer o que a pessoa pode, e quando foi tirada
|
|
2
|
+
--
|
|
3
|
+
-- A 092 reescreveu `app_config()` para devolver o preset do ofício — e, ao
|
|
4
|
+
-- repetir a função inteira, deixou cair duas chaves que a 001 entregava:
|
|
5
|
+
--
|
|
6
|
+
-- permissions a cerca do cliente: sem ela a tela monta sem saber o que
|
|
7
|
+
-- esconder, e todo `can()` responde pelo padrão
|
|
8
|
+
-- generated_at a idade da resposta: sem ela o snapshot em localStorage
|
|
9
|
+
-- não tem como saber se ainda vale
|
|
10
|
+
--
|
|
11
|
+
-- Nenhuma das duas some com estrondo. A tela abre, a marca pinta, o rail
|
|
12
|
+
-- desenha — e a cerca fica por conta do padrão enquanto o cache nunca vence.
|
|
13
|
+
-- É a forma mais cara de erro: a que parece estar funcionando.
|
|
14
|
+
--
|
|
15
|
+
-- Repetida inteira de novo, que é como uma função volta a ter dono. Só as
|
|
16
|
+
-- duas chaves mudam; o resto é byte a byte o que a 092 deixou.
|
|
17
|
+
--
|
|
18
|
+
-- Nota sobre `plugins`: ela continua devolvendo TODAS as linhas, inclusive as
|
|
19
|
+
-- desligadas e as de teste vencido (com `status` já resolvido para
|
|
20
|
+
-- 'disabled'). Isso não é descuido — é o contrato novo. A resolução no cliente
|
|
21
|
+
-- é ADITIVA: linha ausente cai no padrão do manifesto, que é LIGADO. Uma conta
|
|
22
|
+
-- só consegue dizer "isto eu não uso" se a linha que diz isso chegar.
|
|
23
|
+
--
|
|
24
|
+
-- Numerada DEPOIS da 113, de propósito: aquela vem do outro ramo e reemite
|
|
25
|
+
-- `app_config()` na linhagem de lá — sem a guarda do schema `platform` e sem
|
|
26
|
+
-- as duas chaves. Quem define por último é quem define; renumerar custa nada e
|
|
27
|
+
-- deixa o arquivo do outro ramo byte a byte como ele é lá.
|
|
28
|
+
--
|
|
29
|
+
-- Idempotente.
|
|
30
|
+
|
|
31
|
+
CREATE OR REPLACE FUNCTION public.app_config()
|
|
32
|
+
RETURNS jsonb
|
|
33
|
+
LANGUAGE plpgsql
|
|
34
|
+
STABLE SECURITY DEFINER
|
|
35
|
+
SET search_path TO ''
|
|
36
|
+
AS $function$
|
|
37
|
+
DECLARE
|
|
38
|
+
v_tenant uuid := app.current_tenant_id();
|
|
39
|
+
v_staff text;
|
|
40
|
+
BEGIN
|
|
41
|
+
-- `platform` e o schema do control-admin: existe no cluster que hospeda a
|
|
42
|
+
-- plataforma e NAO na cadeia canonica, onde esta funcao e reconstruida do
|
|
43
|
+
-- zero. Chamar direto compila (o corpo de plpgsql so resolve nomes na
|
|
44
|
+
-- primeira execucao) e estoura na primeira chamada com "schema platform
|
|
45
|
+
-- does not exist" — que e o pedagio de `search_path` vazio ja pago na
|
|
46
|
+
-- 060. Perguntar antes deixa a mesma definicao servir aos dois: sem o
|
|
47
|
+
-- schema, ninguem e staff, e o control-admin simplesmente nao e oferecido.
|
|
48
|
+
IF pg_catalog.to_regproc('platform.staff_role(uuid)') IS NOT NULL THEN
|
|
49
|
+
EXECUTE 'SELECT platform.staff_role($1)' INTO v_staff USING (SELECT auth.uid());
|
|
50
|
+
END IF;
|
|
51
|
+
|
|
52
|
+
IF v_tenant IS NULL THEN
|
|
53
|
+
RETURN jsonb_build_object('tenant_id', NULL, 'plugins', '[]'::jsonb, 'nav', '[]'::jsonb,
|
|
54
|
+
'apps', '[]'::jsonb, 'permissions', '[]'::jsonb, 'generated_at', now());
|
|
55
|
+
END IF;
|
|
56
|
+
|
|
57
|
+
RETURN jsonb_build_object(
|
|
58
|
+
'tenant_id', v_tenant,
|
|
59
|
+
'branding', coalesce(app.brand_of(v_tenant), '{}'::jsonb),
|
|
60
|
+
'settings', public.tenant_settings(),
|
|
61
|
+
-- Todas as linhas, inclusive as desligadas: a regra no cliente é aditiva, e
|
|
62
|
+
-- uma linha ausente cai no padrão do manifesto — que é LIGADO.
|
|
63
|
+
'plugins', coalesce((
|
|
64
|
+
SELECT jsonb_agg(jsonb_build_object('plugin', t.plugin_id, 'facet', nullif(t.facet, ''),
|
|
65
|
+
'status', CASE WHEN t.expires_at IS NOT NULL AND t.expires_at <= now() THEN 'disabled' ELSE t.status END,
|
|
66
|
+
'source', t.source, 'config', t.config, 'expires_at', t.expires_at)
|
|
67
|
+
ORDER BY t.plugin_id, t.facet)
|
|
68
|
+
FROM app.tenant_plugins t WHERE t.tenant_id = v_tenant), '[]'::jsonb),
|
|
69
|
+
'apps', coalesce((
|
|
70
|
+
SELECT jsonb_agg(x ORDER BY x ->> 'house', x ->> 'name') FROM (
|
|
71
|
+
SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
|
|
72
|
+
'url', a.url, 'house', false, 'preset', a.preset) AS x
|
|
73
|
+
FROM app.tenant_apps ta JOIN app.apps a ON a.id = ta.app_id
|
|
74
|
+
WHERE ta.tenant_id = v_tenant AND ta.status = 'active' AND a.active
|
|
75
|
+
UNION ALL
|
|
76
|
+
SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
|
|
77
|
+
'url', a.url, 'house', true, 'preset', a.preset) AS x
|
|
78
|
+
FROM app.apps a WHERE v_staff IS NOT NULL AND a.id = 'control-admin' AND a.active AND a.url IS NOT NULL) apps), '[]'::jsonb),
|
|
79
|
+
'nav', coalesce((
|
|
80
|
+
SELECT jsonb_agg(jsonb_build_object('key', n.entry_key, 'hidden', n.hidden, 'label', n.label,
|
|
81
|
+
'section', n.section, 'position', n.position) ORDER BY n.entry_key)
|
|
82
|
+
FROM app.tenant_nav n WHERE n.tenant_id = v_tenant), '[]'::jsonb),
|
|
83
|
+
-- As duas que a reescrita da 092 deixou cair, de volta com a mesma forma
|
|
84
|
+
-- da 001: o cliente monta a cerca com `permissions` e decide a validade do
|
|
85
|
+
-- snapshot com `generated_at`.
|
|
86
|
+
'permissions', coalesce((
|
|
87
|
+
SELECT jsonb_agg(DISTINCT rp.permission)
|
|
88
|
+
FROM app.memberships m
|
|
89
|
+
JOIN app.role_permissions rp ON rp.role_id = m.role_id AND rp.tenant_id = m.tenant_id
|
|
90
|
+
WHERE m.tenant_id = v_tenant AND m.user_id = (SELECT auth.uid()) AND m.active
|
|
91
|
+
), '[]'::jsonb),
|
|
92
|
+
'generated_at', now());
|
|
93
|
+
END $function$;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 115_o_rail_e_de_quem_tem_um.sql
|
|
3
|
+
-- ---------------------------------------------------------------------------
|
|
4
|
+
-- O ControlGroup roda dois aplicativos — o FullControl, que é o sistema dele, e
|
|
5
|
+
-- o FiscalControl, que é a retaguarda contábil. O rail que ele desenhava era o
|
|
6
|
+
-- de NINGUÉM: linhas soltas, sem as rubricas que o FullControl declara.
|
|
7
|
+
--
|
|
8
|
+
-- ── Por que ─────────────────────────────────────────────────────────────────
|
|
9
|
+
-- `useAppPreset` usa o preset do PRIMEIRO aplicativo da lista, e com razão: um
|
|
10
|
+
-- rail com as duas árvores não é a soma de dois sistemas, é nenhum dos dois.
|
|
11
|
+
-- Quem ordena a lista é esta função, e ela ordenava assim:
|
|
12
|
+
--
|
|
13
|
+
-- ORDER BY house, trade DESC, name
|
|
14
|
+
--
|
|
15
|
+
-- `trade` é "tem ofício" (vertical fora de `accounting`/`generic`). Nem o
|
|
16
|
+
-- `full` nem o `fiscal` têm: um é o ERP genérico e o outro é contabilidade. Com
|
|
17
|
+
-- os dois empatados em `trade`, o desempate caía no NOME — e "FiscalControl"
|
|
18
|
+
-- vem antes de "FullControl" no alfabeto.
|
|
19
|
+
--
|
|
20
|
+
-- Então o aplicativo sem opinião nenhuma sobre o rail (o `fiscal` não declara
|
|
21
|
+
-- `preset.rail`) vencia o que declara a árvore inteira, por uma letra.
|
|
22
|
+
--
|
|
23
|
+
-- ── O conserto ──────────────────────────────────────────────────────────────
|
|
24
|
+
-- Um critério a mais, ANTES do nome: quem TEM rail ganha de quem não tem.
|
|
25
|
+
--
|
|
26
|
+
-- Não é um caso particular do ControlGroup. Um aplicativo sem `preset.rail` é
|
|
27
|
+
-- um que não opina sobre a navegação — a retaguarda, o complemento —, e ele
|
|
28
|
+
-- nunca deveria calar o que opina. O alfabeto não sabe disso; esta linha sabe.
|
|
29
|
+
--
|
|
30
|
+
-- `trade` continua vindo primeiro: entre dois que declaram rail, quem tem
|
|
31
|
+
-- ofício manda, que é o que a 084 resolveu para o Artorius.
|
|
32
|
+
--
|
|
33
|
+
-- Idempotente: reemite a função.
|
|
34
|
+
-- ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
CREATE OR REPLACE FUNCTION public.app_config()
|
|
37
|
+
RETURNS jsonb
|
|
38
|
+
LANGUAGE plpgsql
|
|
39
|
+
STABLE SECURITY DEFINER
|
|
40
|
+
SET search_path TO ''
|
|
41
|
+
AS $function$
|
|
42
|
+
DECLARE
|
|
43
|
+
v_tenant uuid := app.current_tenant_id();
|
|
44
|
+
v_staff text;
|
|
45
|
+
BEGIN
|
|
46
|
+
-- `platform` é do CLUSTER, não da cadeia canônica: um banco montado do zero
|
|
47
|
+
-- (o db-from-zero, um pool novo) não tem esse schema, e uma chamada direta
|
|
48
|
+
-- estoura na inicialização da variável — antes de qualquer linha do corpo.
|
|
49
|
+
-- Mesma guarda que a 092 já precisou pôr aqui; reemitir a função copiando a
|
|
50
|
+
-- definição VIVA do cluster a trouxe de volta.
|
|
51
|
+
IF pg_catalog.to_regproc('platform.staff_role(uuid)') IS NOT NULL THEN
|
|
52
|
+
EXECUTE 'SELECT platform.staff_role($1)' INTO v_staff USING (SELECT auth.uid());
|
|
53
|
+
END IF;
|
|
54
|
+
|
|
55
|
+
IF v_tenant IS NULL THEN
|
|
56
|
+
RETURN jsonb_build_object('tenant_id', NULL, 'plugins', '[]'::jsonb, 'nav', '[]'::jsonb,
|
|
57
|
+
'apps', '[]'::jsonb, 'permissions', '[]'::jsonb, 'generated_at', now());
|
|
58
|
+
END IF;
|
|
59
|
+
|
|
60
|
+
RETURN jsonb_build_object(
|
|
61
|
+
'tenant_id', v_tenant,
|
|
62
|
+
'branding', coalesce(app.brand_of(v_tenant), '{}'::jsonb),
|
|
63
|
+
'settings', public.tenant_settings(),
|
|
64
|
+
'plugins', coalesce((
|
|
65
|
+
SELECT jsonb_agg(jsonb_build_object('plugin', t.plugin_id, 'facet', nullif(t.facet, ''),
|
|
66
|
+
'status', CASE WHEN t.expires_at IS NOT NULL AND t.expires_at <= now() THEN 'disabled' ELSE t.status END,
|
|
67
|
+
'source', t.source, 'config', t.config, 'expires_at', t.expires_at)
|
|
68
|
+
ORDER BY t.plugin_id, t.facet)
|
|
69
|
+
FROM app.tenant_plugins t WHERE t.tenant_id = v_tenant), '[]'::jsonb),
|
|
70
|
+
-- `trade` ordena a lista: quem roda dois aplicativos tem UM ofício, e o
|
|
71
|
+
-- outro é retaguarda. FiscalControl vinha antes de StoreControl só por
|
|
72
|
+
-- ordem alfabética, e o Artorius desenhava o rail de ninguém.
|
|
73
|
+
--
|
|
74
|
+
-- E entre dois SEM ofício — o ERP genérico e a contabilidade —, quem
|
|
75
|
+
-- declara `preset.rail` vence quem não declara: um aplicativo que não opina
|
|
76
|
+
-- sobre a navegação não pode calar o que opina só por vir antes no
|
|
77
|
+
-- alfabeto (115).
|
|
78
|
+
'apps', coalesce((
|
|
79
|
+
SELECT jsonb_agg(x ORDER BY x ->> 'house',
|
|
80
|
+
(x ->> 'trade')::boolean DESC,
|
|
81
|
+
(x -> 'preset' -> 'rail' IS NOT NULL) DESC,
|
|
82
|
+
x ->> 'name') FROM (
|
|
83
|
+
SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
|
|
84
|
+
'url', a.url, 'house', false, 'preset', a.preset, 'vertical_id', a.vertical_id,
|
|
85
|
+
'trade', a.vertical_id IS NOT NULL AND a.vertical_id NOT IN ('accounting','generic')) AS x
|
|
86
|
+
FROM app.tenant_apps ta JOIN app.apps a ON a.id = ta.app_id
|
|
87
|
+
WHERE ta.tenant_id = v_tenant AND ta.status = 'active' AND a.active
|
|
88
|
+
UNION ALL
|
|
89
|
+
SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
|
|
90
|
+
'url', a.url, 'house', true, 'preset', a.preset, 'vertical_id', a.vertical_id,
|
|
91
|
+
'trade', false) AS x
|
|
92
|
+
FROM app.apps a WHERE v_staff IS NOT NULL AND a.id = 'control-admin' AND a.active AND a.url IS NOT NULL) apps), '[]'::jsonb),
|
|
93
|
+
'nav', coalesce((
|
|
94
|
+
SELECT jsonb_agg(jsonb_build_object('key', n.entry_key, 'hidden', n.hidden, 'label', n.label,
|
|
95
|
+
'section', n.section, 'position', n.position) ORDER BY n.entry_key)
|
|
96
|
+
FROM app.tenant_nav n WHERE n.tenant_id = v_tenant), '[]'::jsonb),
|
|
97
|
+
-- As duas que a 114 devolveu, e que esta migration quase derrubou de novo:
|
|
98
|
+
-- copiei a definição VIVA do cluster, onde a 114 ainda não rodou. É a
|
|
99
|
+
-- terceira vez que repetir esta função inteira perde uma chave dela — quem
|
|
100
|
+
-- a reemite tem de começar pelo ARQUIVO da última, não pelo banco.
|
|
101
|
+
'permissions', coalesce((
|
|
102
|
+
SELECT jsonb_agg(DISTINCT rp.permission)
|
|
103
|
+
FROM app.memberships m
|
|
104
|
+
JOIN app.role_permissions rp ON rp.role_id = m.role_id AND rp.tenant_id = m.tenant_id
|
|
105
|
+
WHERE m.tenant_id = v_tenant AND m.user_id = (SELECT auth.uid()) AND m.active
|
|
106
|
+
), '[]'::jsonb),
|
|
107
|
+
'generated_at', now());
|
|
108
|
+
END $function$;
|
package/package.json
CHANGED