@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,32 @@
|
|
|
1
|
+
-- Quem é profissional continua agendável, mesmo sendo também funcionário.
|
|
2
|
+
--
|
|
3
|
+
-- O reparo da 075 desmarcou `is_bookable` para toda pessoa com linha de ledger
|
|
4
|
+
-- vinda de `employees`, `clients` ou `contacts`. A condição parecia certa e
|
|
5
|
+
-- tinha um furo: **uma pessoa pode ter MAIS DE UMA linha no ledger.**
|
|
6
|
+
--
|
|
7
|
+
-- O V1 tem a mesma pessoa em duas tabelas quando ela atende E é registrada
|
|
8
|
+
-- como funcionária — o que é comum numa clínica: a profissional que também
|
|
9
|
+
-- está na folha. Medido em 20/09/2026, tenant Espaço Facial: 20 pessoas com
|
|
10
|
+
-- duas origens, e **18 profissionais desmarcados** por causa da linha de
|
|
11
|
+
-- funcionário delas.
|
|
12
|
+
--
|
|
13
|
+
-- O efeito era visível na tela: a agenda do Treinamento mostrava 5 colunas
|
|
14
|
+
-- onde o V1 lista 14, e as que faltavam eram exatamente essas.
|
|
15
|
+
--
|
|
16
|
+
-- ── A regra que faltava dizer ───────────────────────────────────────────────
|
|
17
|
+
--
|
|
18
|
+
-- Entre duas origens, quem manda sobre "aparece na agenda" é a que diz SIM.
|
|
19
|
+
-- Ser profissional é o que torna alguém agendável; estar na folha não
|
|
20
|
+
-- desfaz isso. A ausência de uma regra de precedência fez a última linha lida
|
|
21
|
+
-- vencer, que é o pior critério possível — ele depende da ordem do porte.
|
|
22
|
+
--
|
|
23
|
+
-- Idempotente.
|
|
24
|
+
|
|
25
|
+
UPDATE public.people p
|
|
26
|
+
SET is_bookable = true
|
|
27
|
+
WHERE p.tenant_id IS NOT NULL
|
|
28
|
+
AND NOT p.is_bookable
|
|
29
|
+
AND EXISTS (SELECT 1 FROM migration.ledger l
|
|
30
|
+
WHERE l.target_id = p.id
|
|
31
|
+
AND l.target_table = 'public.people'
|
|
32
|
+
AND l.source_table = 'professionals');
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
-- Quem atende em várias não cabe numa coluna.
|
|
2
|
+
--
|
|
3
|
+
-- A 074 escreveu as tuplas `available_in` só para quem estava com
|
|
4
|
+
-- `people.unit_id` nulo. Ficou pela metade, e a metade que faltou é a que a
|
|
5
|
+
-- view não perdoa. `public.v_bookable_people` diz, na 020:
|
|
6
|
+
--
|
|
7
|
+
-- CASE WHEN p.unit_id IS NOT NULL THEN ARRAY[p.unit_id]
|
|
8
|
+
-- ELSE (as tuplas) END
|
|
9
|
+
--
|
|
10
|
+
-- A coluna VENCE a lista. Então quem o V1 registra em três unidades e chegou
|
|
11
|
+
-- ao V2 com uma na coluna aparece em uma só — e some da agenda das outras
|
|
12
|
+
-- duas. As tuplas que a 074 escrevesse para essa pessoa nunca seriam lidas.
|
|
13
|
+
--
|
|
14
|
+
-- Medido em 20/09/2026: 8 profissionais nessa situação. Na tela, a agenda de
|
|
15
|
+
-- Resende mostrava 3 colunas onde o V1 lista 6, e a de Maricá 3 onde lista 5.
|
|
16
|
+
--
|
|
17
|
+
-- ── O conserto ──────────────────────────────────────────────────────────────
|
|
18
|
+
--
|
|
19
|
+
-- Para quem o V1 lista em MAIS DE UMA unidade: a coluna é limpa e todas as
|
|
20
|
+
-- unidades viram tuplas. É o desenho da 020 aplicado até o fim — coluna para
|
|
21
|
+
-- quem tem uma, lista para quem tem várias.
|
|
22
|
+
--
|
|
23
|
+
-- Quem tem uma só fica como está: a coluna já é a resposta completa, e mexer
|
|
24
|
+
-- nela seria trocar de lugar a mesma informação.
|
|
25
|
+
--
|
|
26
|
+
-- Idempotente.
|
|
27
|
+
|
|
28
|
+
-- 1. As tuplas de TODAS as unidades de quem tem várias — inclusive a que já
|
|
29
|
+
-- estava na coluna, que sem isto se perderia no passo 2.
|
|
30
|
+
INSERT INTO app.resource_grants
|
|
31
|
+
(tenant_id, resource_type, resource_id, relation, subject_type, subject_id)
|
|
32
|
+
SELECT DISTINCT p.tenant_id, 'people.person', p.id, 'available_in', 'unit', lu.target_id
|
|
33
|
+
FROM public.people p
|
|
34
|
+
JOIN migration.ledger lp
|
|
35
|
+
ON lp.target_id = p.id AND lp.source_table = 'professionals'
|
|
36
|
+
AND lp.target_table = 'public.people' AND lp.status = 'migrated'
|
|
37
|
+
CROSS JOIN LATERAL jsonb_array_elements_text(p.metadata #> '{professional,v1_unit_refs}') AS ref(v1)
|
|
38
|
+
JOIN migration.ledger lu
|
|
39
|
+
ON lu.tenant_id = p.tenant_id AND lu.source_table = 'companies'
|
|
40
|
+
AND lu.target_table = 'app.units' AND lu.status = 'migrated'
|
|
41
|
+
AND lu.source_numeric_id = ref.v1::bigint
|
|
42
|
+
WHERE jsonb_typeof(p.metadata #> '{professional,v1_unit_refs}') = 'array'
|
|
43
|
+
AND jsonb_array_length(p.metadata #> '{professional,v1_unit_refs}') > 1
|
|
44
|
+
AND ref.v1 ~ '^\d+$'
|
|
45
|
+
ON CONFLICT DO NOTHING;
|
|
46
|
+
|
|
47
|
+
-- 2. E a coluna sai do caminho, para a lista ser lida.
|
|
48
|
+
UPDATE public.people p
|
|
49
|
+
SET unit_id = NULL
|
|
50
|
+
WHERE p.unit_id IS NOT NULL
|
|
51
|
+
AND jsonb_typeof(p.metadata #> '{professional,v1_unit_refs}') = 'array'
|
|
52
|
+
AND jsonb_array_length(p.metadata #> '{professional,v1_unit_refs}') > 1
|
|
53
|
+
AND EXISTS (SELECT 1 FROM migration.ledger l
|
|
54
|
+
WHERE l.target_id = p.id AND l.source_table = 'professionals'
|
|
55
|
+
AND l.target_table = 'public.people')
|
|
56
|
+
-- Só se as tuplas existirem de fato: limpar a coluna sem ter a lista
|
|
57
|
+
-- transformaria "atende em três" em "ninguém disse", que aparece em todas.
|
|
58
|
+
AND EXISTS (SELECT 1 FROM app.resource_grants g
|
|
59
|
+
WHERE g.tenant_id = p.tenant_id AND g.resource_type = 'people.person'
|
|
60
|
+
AND g.resource_id = p.id AND g.relation = 'available_in');
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- A unidade do profissional vem de onde ele atende.
|
|
2
|
+
--
|
|
3
|
+
-- Depois da 080 a agenda ainda não batia: Maricá mostrava 3 colunas onde o V1
|
|
4
|
+
-- lista 5. Os dois que faltavam estavam presos a OUTRA unidade.
|
|
5
|
+
--
|
|
6
|
+
-- Leticia Nogueira · V1 units ["600"] (Maricá)
|
|
7
|
+
-- · V2 people.unit_id = outra unidade
|
|
8
|
+
--
|
|
9
|
+
-- `people.unit_id` para quem veio de `professionals` não é preenchido a partir
|
|
10
|
+
-- de `units` — o de-para manda `units` para metadata, e a coluna chega por
|
|
11
|
+
-- outro caminho. E como `public.v_bookable_people` prefere a coluna à lista
|
|
12
|
+
-- (ver 020), essa unidade errada apaga a certa.
|
|
13
|
+
--
|
|
14
|
+
-- ── A regra ─────────────────────────────────────────────────────────────────
|
|
15
|
+
--
|
|
16
|
+
-- Onde o profissional atende é o que o V1 diz em `units`, e nada mais. Quando
|
|
17
|
+
-- essa lista tem exatamente UMA unidade, ela vai para a coluna — que é o
|
|
18
|
+
-- caminho barato e o que a 020 desenhou. A 080 já cuidou de quem tem várias.
|
|
19
|
+
--
|
|
20
|
+
-- Só toca em quem veio de `professionals` E tem a lista em metadata: sem a
|
|
21
|
+
-- lista não há o que afirmar, e sobrescrever a coluna com um palpite seria
|
|
22
|
+
-- trocar um erro por outro.
|
|
23
|
+
--
|
|
24
|
+
-- Idempotente.
|
|
25
|
+
|
|
26
|
+
UPDATE public.people p
|
|
27
|
+
SET unit_id = alvo.unidade
|
|
28
|
+
FROM (
|
|
29
|
+
SELECT p2.id,
|
|
30
|
+
lu.target_id AS unidade
|
|
31
|
+
FROM public.people p2
|
|
32
|
+
JOIN migration.ledger lp
|
|
33
|
+
ON lp.target_id = p2.id
|
|
34
|
+
AND lp.source_table = 'professionals'
|
|
35
|
+
AND lp.target_table = 'public.people'
|
|
36
|
+
JOIN migration.ledger lu
|
|
37
|
+
ON lu.tenant_id = p2.tenant_id
|
|
38
|
+
AND lu.source_table = 'companies'
|
|
39
|
+
AND lu.target_table = 'app.units'
|
|
40
|
+
AND lu.status = 'migrated'
|
|
41
|
+
AND lu.source_numeric_id = (p2.metadata #>> '{professional,v1_unit_refs,0}')::bigint
|
|
42
|
+
WHERE jsonb_typeof(p2.metadata #> '{professional,v1_unit_refs}') = 'array'
|
|
43
|
+
AND jsonb_array_length(p2.metadata #> '{professional,v1_unit_refs}') = 1
|
|
44
|
+
AND (p2.metadata #>> '{professional,v1_unit_refs,0}') ~ '^\d+$'
|
|
45
|
+
) alvo
|
|
46
|
+
WHERE alvo.id = p.id
|
|
47
|
+
AND p.unit_id IS DISTINCT FROM alvo.unidade;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
-- A ponte de clientes volta a existir.
|
|
2
|
+
--
|
|
3
|
+
-- O painel pedia `public.v_clients` e levava **404** em toda carga — a view não
|
|
4
|
+
-- existe. Quatro cartões de cliente ficavam vazios e o console acusava a falha
|
|
5
|
+
-- quatro vezes por visita.
|
|
6
|
+
--
|
|
7
|
+
-- Ela foi removida com razão: juntava `public.clients` com `saas_core.persons`,
|
|
8
|
+
-- e as duas deixaram de existir quando a pessoa virou `public.people`. O que
|
|
9
|
+
-- ficou para trás foi o CÓDIGO — `packages/core/src/data/archetype.ts:182`
|
|
10
|
+
-- mapeia `clients → v_clients`, e o painel do StudioControl a chama em quatro
|
|
11
|
+
-- lugares. Um mapa apontando para um lugar demolido.
|
|
12
|
+
--
|
|
13
|
+
-- Esta migration reconstrói a ponte sobre o que existe hoje, com as mesmas
|
|
14
|
+
-- colunas que o consumidor pede.
|
|
15
|
+
--
|
|
16
|
+
-- ── As três colunas derivadas ───────────────────────────────────────────────
|
|
17
|
+
--
|
|
18
|
+
-- `visits`, `total_spent` e `last_visit` não são campos de pessoa: são
|
|
19
|
+
-- consequência do histórico. Vêm de LATERAL correlacionado por `party_id`, e é
|
|
20
|
+
-- por isso que a migration cria também o índice — sem ele cada cliente pagaria
|
|
21
|
+
-- uma varredura dos 199 mil agendamentos, e o cartão de retenção (que filtra
|
|
22
|
+
-- `visits > 1`) levaria o painel junto.
|
|
23
|
+
--
|
|
24
|
+
-- `security_invoker` OBRIGATÓRIO: sem ele a view roda com os direitos do dono e
|
|
25
|
+
-- as políticas de `public.people` não são avaliadas — que é exatamente como
|
|
26
|
+
-- views vazaram entre tenants antes.
|
|
27
|
+
--
|
|
28
|
+
-- Idempotente.
|
|
29
|
+
|
|
30
|
+
CREATE INDEX IF NOT EXISTS appointments_party_idx
|
|
31
|
+
ON public.appointments (tenant_id, party_id)
|
|
32
|
+
WHERE party_id IS NOT NULL;
|
|
33
|
+
|
|
34
|
+
COMMENT ON INDEX public.appointments_party_idx IS
|
|
35
|
+
'Quantas vezes esta pessoa veio. O caminho de v_clients.visits e de toda '
|
|
36
|
+
'tela que conta o histórico de um cliente.';
|
|
37
|
+
|
|
38
|
+
DROP VIEW IF EXISTS public.v_clients;
|
|
39
|
+
|
|
40
|
+
CREATE VIEW public.v_clients
|
|
41
|
+
WITH (security_invoker = true)
|
|
42
|
+
AS
|
|
43
|
+
SELECT
|
|
44
|
+
p.id,
|
|
45
|
+
p.tenant_id,
|
|
46
|
+
p.name,
|
|
47
|
+
p.email,
|
|
48
|
+
p.phone,
|
|
49
|
+
p.document_number,
|
|
50
|
+
p.avatar_url,
|
|
51
|
+
p.date_of_birth,
|
|
52
|
+
p.notes,
|
|
53
|
+
p.is_active,
|
|
54
|
+
p.tags,
|
|
55
|
+
p.unit_id,
|
|
56
|
+
p.custom_fields ->> 'gender' AS gender,
|
|
57
|
+
p.metadata #>> '{crm,origin}' AS origin,
|
|
58
|
+
coalesce(h.visits, 0)::integer AS visits,
|
|
59
|
+
coalesce(g.total_spent, 0)::numeric(14,2) AS total_spent,
|
|
60
|
+
h.last_visit,
|
|
61
|
+
p.created_at,
|
|
62
|
+
p.updated_at
|
|
63
|
+
FROM public.people p
|
|
64
|
+
-- Correlacionado em `p.id`: só os agendamentos DESTA pessoa, pelo índice
|
|
65
|
+
-- acima. Um agregado sem correlação aqui seria refeito por cliente.
|
|
66
|
+
LEFT JOIN LATERAL (
|
|
67
|
+
SELECT count(*) AS visits, max(a.starts_at) AS last_visit
|
|
68
|
+
FROM public.appointments a
|
|
69
|
+
WHERE a.tenant_id = p.tenant_id
|
|
70
|
+
AND a.party_id = p.id
|
|
71
|
+
AND a.status = 'completed'
|
|
72
|
+
) h ON true
|
|
73
|
+
LEFT JOIN LATERAL (
|
|
74
|
+
SELECT sum(o.total) AS total_spent
|
|
75
|
+
FROM public.orders o
|
|
76
|
+
WHERE o.tenant_id = p.tenant_id
|
|
77
|
+
AND o.party_id = p.id
|
|
78
|
+
AND o.status = 'completed'
|
|
79
|
+
) g ON true
|
|
80
|
+
WHERE p.kind = 'customer'
|
|
81
|
+
AND p.merged_into_id IS NULL;
|
|
82
|
+
|
|
83
|
+
COMMENT ON VIEW public.v_clients IS
|
|
84
|
+
'A pessoa como CLIENTE, com o que o histórico dela diz: quantas vezes veio, '
|
|
85
|
+
'quanto gastou, quando foi a última. Ponte para o código que ainda fala o '
|
|
86
|
+
'vocabulário antigo (archetype.ts mapeia clients → v_clients). O nome '
|
|
87
|
+
'sobreviveu à tabela que o originou.';
|
|
88
|
+
|
|
89
|
+
GRANT SELECT ON public.v_clients TO authenticated, service_role;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
-- A liberação pergunta ao índice antes de chamar a função
|
|
2
|
+
--
|
|
3
|
+
-- A 070 pôs dentro de `app.available_in_my_units` a sondagem por RECURSO: sem
|
|
4
|
+
-- tupla nomeando esta linha, devolve `true` sem montar CTE nenhum. A conta
|
|
5
|
+
-- ficou certa e o tempo não: a função é SECURITY DEFINER, e o PostgreSQL NUNCA
|
|
6
|
+
-- incorpora função de definidor no plano. Então cada linha paga a montagem de
|
|
7
|
+
-- um executor inteiro só para chegar naquele `true`.
|
|
8
|
+
--
|
|
9
|
+
-- Com 120.361 pessoas no Espaço Facial isso é o custo da tela de Leads inteira.
|
|
10
|
+
-- Medido, a mesma consulta com e sem esta migração:
|
|
11
|
+
--
|
|
12
|
+
-- lista de leads, unidade Millenium . . . 42.257 ms → 1.144 ms
|
|
13
|
+
-- lista de leads, rede . . . . . . . . . 47.498 ms → 5.894 ms
|
|
14
|
+
--
|
|
15
|
+
-- A sondagem sobe para a política, onde ela é expressão e não chamada: o
|
|
16
|
+
-- planejador a incorpora e ela vira uma varredura de índice no prefixo
|
|
17
|
+
-- (tenant_id, resource_type, resource_id) da PK de `app.resource_grants` —
|
|
18
|
+
-- tabela de 1.656 linhas. Quem não tem concessão alguma (118.9 mil das 120.4
|
|
19
|
+
-- mil pessoas) é liberado ali e a função definidora não chega a ser chamada.
|
|
20
|
+
--
|
|
21
|
+
-- `app.has_release_tuple` NÃO é SECURITY DEFINER, e é de propósito: é o que
|
|
22
|
+
-- permite incorporar. Ela lê `app.resource_grants` com os direitos de quem
|
|
23
|
+
-- pergunta, e a política de leitura daquela tabela é o próprio tenant — a
|
|
24
|
+
-- mesma linha que a função definidora enxergaria.
|
|
25
|
+
--
|
|
26
|
+
-- ── O que esta migração NÃO faz, e por quê ──────────────────────────────────
|
|
27
|
+
--
|
|
28
|
+
-- Uma primeira versão deste arquivo mexia em sete políticas de uma vez
|
|
29
|
+
-- (`people`, `products`, `services`, `price_table_items`, `product_variants`,
|
|
30
|
+
-- `service_packages`, `service_package_items`) e descobria o tipo do recurso
|
|
31
|
+
-- lendo a expressão da política com uma regex. Foi um erro, e vale escrever
|
|
32
|
+
-- qual: a expressão tem duas formas — `available_in_my_units('catalog.product',
|
|
33
|
+
-- …)` e `available_in_my_units((SELECT app.current_tenant_id()),
|
|
34
|
+
-- 'catalog.product', …)`. A regex casava só a primeira, e `regexp_replace`
|
|
35
|
+
-- devolve a ENTRADA INTEIRA quando não casa. O tipo do recurso virava o texto
|
|
36
|
+
-- completo da política antiga, `g.resource_type` nunca casava com nada, o
|
|
37
|
+
-- `NOT EXISTS` dava sempre verdadeiro e a linha era liberada para todo mundo.
|
|
38
|
+
-- Cinco asserções de `075_catalog/006_distribution_polarity` pegaram isso — o
|
|
39
|
+
-- "deny wins" parou de vencer.
|
|
40
|
+
--
|
|
41
|
+
-- Então: uma tabela só, a que foi medida, e o tipo escrito por extenso. Não há
|
|
42
|
+
-- texto de SQL sendo interpretado em lugar nenhum deste arquivo.
|
|
43
|
+
--
|
|
44
|
+
-- Idempotente.
|
|
45
|
+
|
|
46
|
+
CREATE OR REPLACE FUNCTION app.has_release_tuple(p_type text, p_id uuid)
|
|
47
|
+
RETURNS boolean
|
|
48
|
+
LANGUAGE sql
|
|
49
|
+
STABLE
|
|
50
|
+
SET search_path TO ''
|
|
51
|
+
AS $function$
|
|
52
|
+
SELECT EXISTS (
|
|
53
|
+
SELECT 1 FROM app.resource_grants g
|
|
54
|
+
WHERE g.tenant_id = (SELECT app.current_tenant_id())
|
|
55
|
+
AND g.resource_type = p_type
|
|
56
|
+
AND g.resource_id = p_id
|
|
57
|
+
AND g.relation IN ('available_in', 'unavailable_in')
|
|
58
|
+
);
|
|
59
|
+
$function$;
|
|
60
|
+
|
|
61
|
+
REVOKE ALL ON FUNCTION app.has_release_tuple(text, uuid) FROM PUBLIC, anon;
|
|
62
|
+
GRANT EXECUTE ON FUNCTION app.has_release_tuple(text, uuid) TO authenticated, service_role;
|
|
63
|
+
|
|
64
|
+
DO $$
|
|
65
|
+
BEGIN
|
|
66
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policy WHERE polname = 'people_available_in') THEN
|
|
67
|
+
RAISE NOTICE 'sem people_available_in neste cluster, nada a fazer';
|
|
68
|
+
RETURN;
|
|
69
|
+
END IF;
|
|
70
|
+
|
|
71
|
+
ALTER POLICY people_available_in ON public.people USING (
|
|
72
|
+
(NOT (SELECT app.grants_exist('people.person', ARRAY['available_in', 'unavailable_in'])))
|
|
73
|
+
OR (unit_id IS NOT NULL)
|
|
74
|
+
-- A pergunta barata, incorporável, antes da cara. O ramo acrescentado é,
|
|
75
|
+
-- palavra por palavra, a segunda guarda da própria função.
|
|
76
|
+
OR (NOT app.has_release_tuple('people.person', people.id))
|
|
77
|
+
OR (SELECT app.available_in_my_units('people.person', people.id))
|
|
78
|
+
);
|
|
79
|
+
END $$;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- Quem pode executar o que o porte criou
|
|
2
|
+
--
|
|
3
|
+
-- Função nova nasce `EXECUTE` para PUBLIC, e `anon` herda dali. A postura N15c
|
|
4
|
+
-- da bancada diz o que isso significa, e estava vermelha:
|
|
5
|
+
--
|
|
6
|
+
-- N15c: anon can execute nothing callable in schema app have: 9 want: 0
|
|
7
|
+
--
|
|
8
|
+
-- As que este ramo criou são auxiliares chamadas de
|
|
9
|
+
-- DENTRO de funções SECURITY DEFINER — resolvem identidade com os direitos do
|
|
10
|
+
-- dono e ninguém as chama por fora. A última, `app.has_release_tuple`, é chamada
|
|
11
|
+
-- de FORA: a 087 a pôs dentro de `people_available_in` justamente para o
|
|
12
|
+
-- planejador poder incorporá-la, e isso exige que quem lê a tabela possa
|
|
13
|
+
-- executá-la.
|
|
14
|
+
--
|
|
15
|
+
-- A lista é por extenso, e isso é a lição deste arquivo: uma primeira versão
|
|
16
|
+
-- varria `pg_proc` inteiro nos esquemas `app` e `migration` e revogava de
|
|
17
|
+
-- todos, devolvendo a `authenticated` só uma lista curta que eu supus ser
|
|
18
|
+
-- suficiente. Não era: `app.grants_exist` é citada em política e ficou sem
|
|
19
|
+
-- concessão, e 35 suítes caíram com `permission denied for function
|
|
20
|
+
-- grants_exist`. Revogar em massa é fácil de escrever e impossível de revisar.
|
|
21
|
+
-- Aqui só entram os nomes que este ramo trouxe.
|
|
22
|
+
--
|
|
23
|
+
-- Idempotente.
|
|
24
|
+
|
|
25
|
+
DO $$
|
|
26
|
+
DECLARE
|
|
27
|
+
v_fn text;
|
|
28
|
+
BEGIN
|
|
29
|
+
FOR v_fn IN
|
|
30
|
+
SELECT p.oid::regprocedure::text
|
|
31
|
+
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
32
|
+
WHERE n.nspname = 'app'
|
|
33
|
+
AND p.proname IN (
|
|
34
|
+
'_current_tenant_id_raw', '_current_unit_id_raw', '_has_permission_raw',
|
|
35
|
+
'_has_unit_raw', '_owner_scoped_raw', '_unit_scoping_on_raw',
|
|
36
|
+
'requested_unit')
|
|
37
|
+
LOOP
|
|
38
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC, anon, authenticated', v_fn);
|
|
39
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', v_fn);
|
|
40
|
+
END LOOP;
|
|
41
|
+
|
|
42
|
+
FOR v_fn IN
|
|
43
|
+
SELECT p.oid::regprocedure::text
|
|
44
|
+
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
45
|
+
WHERE n.nspname = 'app' AND p.proname = 'has_release_tuple'
|
|
46
|
+
LOOP
|
|
47
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC, anon', v_fn);
|
|
48
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO authenticated, service_role', v_fn);
|
|
49
|
+
END LOOP;
|
|
50
|
+
END $$;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
-- A memória de transação não serve para isto
|
|
2
|
+
--
|
|
3
|
+
-- A 080, a 081 e a 082 puseram o contexto da requisição numa GUC de transação
|
|
4
|
+
-- (`set_config(..., true)`) para não resolvê-lo uma vez por linha. A ideia era
|
|
5
|
+
-- boa e o ganho parecia real. A bancada mostrou, em quarenta e uma asserções e
|
|
6
|
+
-- por três caminhos diferentes, que a premissa não se sustenta: uma transação
|
|
7
|
+
-- NÃO é uma pergunta só.
|
|
8
|
+
--
|
|
9
|
+
-- Primeiro a identidade. A bancada troca `request.jwt.claims` entre asserções
|
|
10
|
+
-- — é o que ela precisa fazer para provar que a claim de outro inquilino não
|
|
11
|
+
-- resolve —, e a segunda pergunta recebia a resposta da primeira:
|
|
12
|
+
--
|
|
13
|
+
-- not ok 2 - a claim naming another tenant resolves to no tenant
|
|
14
|
+
-- have: 3fc8bcfd-923c-8f1e-72cf-c8d5dece7bbd want: NULL
|
|
15
|
+
--
|
|
16
|
+
-- Depois o dado. `UPDATE public.tenants SET unit_scoping_enabled = false` e,
|
|
17
|
+
-- na linha seguinte, `app.unit_scoping_on()` ainda dizia `true`. Vale para
|
|
18
|
+
-- permissão concedida e lida na mesma transação, e para configuração criada e
|
|
19
|
+
-- consultada em seguida — três suítes, três caminhos, todos realistas: uma RPC
|
|
20
|
+
-- que concede e lê é coisa comum.
|
|
21
|
+
--
|
|
22
|
+
-- Houve uma tentativa intermediária de salvar a ideia: digerir o pedido
|
|
23
|
+
-- (claims × cabeçalhos × papel) e invalidar a memória quando a digestão
|
|
24
|
+
-- mudasse. Resolvia a identidade e não resolvia o dado — a digestão resume o
|
|
25
|
+
-- PEDIDO, e o que muda é o BANCO. E mesmo para a identidade ficou incompleta:
|
|
26
|
+
-- `plugin-crm/002_the_lead_offer_cascades` continuou vermelha, porque quem
|
|
27
|
+
-- troca de identidade ali é uma função, por dentro, sem tocar em claim
|
|
28
|
+
-- nenhuma. Medido: com as envolucradoras memorizadas, a suíte aborta; sem
|
|
29
|
+
-- elas, passa inteira.
|
|
30
|
+
--
|
|
31
|
+
-- Então a memória sai. O que fica das três migrações é o que nelas era
|
|
32
|
+
-- separável e correto: a divisão entre a função pública e a `_raw`, que deixa
|
|
33
|
+
-- a resolução num lugar só.
|
|
34
|
+
--
|
|
35
|
+
-- E o desempenho não vinha daqui. Medido no Espaço Facial depois desta
|
|
36
|
+
-- migração — 273.770 negócios, 190.011 orçamentos, 120.361 pessoas:
|
|
37
|
+
--
|
|
38
|
+
-- lista de orçamentos . . . . . . . . . . 21 ms (índice da 029 do CRM)
|
|
39
|
+
-- quadro do funil, unidade . . . . . . . 443 ms (junção comum da 026)
|
|
40
|
+
-- lista de leads, unidade . . . . . . . 1.144 ms (`has_release_tuple`)
|
|
41
|
+
--
|
|
42
|
+
-- Nenhum desses números dependia de guardar resposta entre linhas.
|
|
43
|
+
--
|
|
44
|
+
-- Idempotente.
|
|
45
|
+
|
|
46
|
+
CREATE OR REPLACE FUNCTION app.has_permission(p_perm text, p_unit uuid DEFAULT NULL::uuid)
|
|
47
|
+
RETURNS boolean
|
|
48
|
+
LANGUAGE sql
|
|
49
|
+
STABLE SECURITY DEFINER
|
|
50
|
+
SET search_path TO ''
|
|
51
|
+
AS $function$ SELECT app._has_permission_raw(p_perm, p_unit) $function$;
|
|
52
|
+
|
|
53
|
+
CREATE OR REPLACE FUNCTION app.has_unit(p_unit uuid)
|
|
54
|
+
RETURNS boolean
|
|
55
|
+
LANGUAGE sql
|
|
56
|
+
STABLE SECURITY DEFINER
|
|
57
|
+
SET search_path TO ''
|
|
58
|
+
AS $function$ SELECT app._has_unit_raw(p_unit) $function$;
|
|
59
|
+
|
|
60
|
+
CREATE OR REPLACE FUNCTION app.owner_scoped(p_resource_type text)
|
|
61
|
+
RETURNS boolean
|
|
62
|
+
LANGUAGE sql
|
|
63
|
+
STABLE SECURITY DEFINER
|
|
64
|
+
SET search_path TO ''
|
|
65
|
+
AS $function$ SELECT app._owner_scoped_raw(p_resource_type) $function$;
|
|
66
|
+
|
|
67
|
+
CREATE OR REPLACE FUNCTION app.unit_scoping_on()
|
|
68
|
+
RETURNS boolean
|
|
69
|
+
LANGUAGE sql
|
|
70
|
+
STABLE SECURITY DEFINER
|
|
71
|
+
SET search_path TO ''
|
|
72
|
+
AS $function$ SELECT app._unit_scoping_on_raw() $function$;
|
|
73
|
+
|
|
74
|
+
CREATE OR REPLACE FUNCTION app.current_tenant_id()
|
|
75
|
+
RETURNS uuid
|
|
76
|
+
LANGUAGE sql
|
|
77
|
+
STABLE SECURITY DEFINER
|
|
78
|
+
SET search_path TO ''
|
|
79
|
+
AS $function$ SELECT app._current_tenant_id_raw() $function$;
|
|
80
|
+
|
|
81
|
+
CREATE OR REPLACE FUNCTION app.current_unit_id()
|
|
82
|
+
RETURNS uuid
|
|
83
|
+
LANGUAGE sql
|
|
84
|
+
STABLE SECURITY DEFINER
|
|
85
|
+
SET search_path TO ''
|
|
86
|
+
AS $function$ SELECT app._current_unit_id_raw() $function$;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 071_a_marca_tambem_conta_como_mudanca_de_configuracao.sql — o carimbo que o
|
|
3
|
+
-- shell consulta passa a enxergar as settings, e a chave do menu passa a ser a
|
|
4
|
+
-- rota (#280, ADR 0023).
|
|
5
|
+
--
|
|
6
|
+
-- ── §1 O carimbo é cego para metade do que ele carimba ─────────────────────
|
|
7
|
+
-- `app_config()` devolve quatro coisas: branding, settings, ativações e menu.
|
|
8
|
+
-- `tenant_config_version()` — o timestamp que o cliente consulta a cada minuto
|
|
9
|
+
-- para saber se vale reler — olha só DUAS delas: `app.tenant_plugins` e
|
|
10
|
+
-- `app.tenant_nav`.
|
|
11
|
+
--
|
|
12
|
+
-- O resultado é que trocar a logo, a cor, o fuso ou a moeda não chega em aba
|
|
13
|
+
-- nenhuma até alguém recarregar. E o defeito não aparece em teste, porque quem
|
|
14
|
+
-- troca a marca está sempre na tela que acabou de trocá-la: é a SEGUNDA aba,
|
|
15
|
+
-- ou o colega ao lado, que fica com o valor velho.
|
|
16
|
+
--
|
|
17
|
+
-- `tenants.updated_at` já sobe sozinha — `tenant_setting_set` e
|
|
18
|
+
-- `tenant_brand_patch` escrevem em `tenants.settings` e o trigger
|
|
19
|
+
-- `public.handle_updated_at()` carimba. Só faltava lerem juntos.
|
|
20
|
+
--
|
|
21
|
+
-- De passagem, o FULL JOIN sai. Ele cruzava as duas tabelas para depois
|
|
22
|
+
-- descartar quase tudo num max(): dezessete linhas de plugin vezes as linhas de
|
|
23
|
+
-- menu do mesmo tenant, por chamada, uma vez por minuto por aba aberta. Três
|
|
24
|
+
-- selects escalares dizem o mesmo e param no índice.
|
|
25
|
+
--
|
|
26
|
+
-- ── §2 A chave do menu é a ROTA ────────────────────────────────────────────
|
|
27
|
+
-- `app.tenant_nav.entry_key` foi comentada como "`<plugin>:<route>` como o
|
|
28
|
+
-- shell compõe" e o shell nunca compôs assim: ele casa por `item.route`. A
|
|
29
|
+
-- tabela está vazia (nenhum tenant compôs menu ainda), então a grafia ainda
|
|
30
|
+
-- pode ser escolhida em vez de migrada — e a rota é a escolha certa por uma
|
|
31
|
+
-- razão que não é de gosto:
|
|
32
|
+
--
|
|
33
|
+
-- Três dos quatro apps da frota esvaziam a navegação dos plugins com
|
|
34
|
+
-- `hideNav()` e declaram o rail em `FayzAppConfig.pages`. Essas linhas NÃO
|
|
35
|
+
-- TÊM plugin. `<plugin>:<route>` não consegue endereçar a maior parte do
|
|
36
|
+
-- menu que existe hoje; a rota endereça tudo, e é o que o usuário vê na URL.
|
|
37
|
+
--
|
|
38
|
+
-- Comentário, não constraint: a chave continua livre para uma entrada que um
|
|
39
|
+
-- dia não seja uma rota (um link externo, um grupo sem destino).
|
|
40
|
+
--
|
|
41
|
+
-- Idempotente e replay-safe. Nada de DDL destrutivo: uma função substituída e
|
|
42
|
+
-- dois comentários.
|
|
43
|
+
-- ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
-- ─────────────────────────────────────────────────────────────────────────
|
|
46
|
+
-- §1 O carimbo enxerga as settings
|
|
47
|
+
-- ─────────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
CREATE OR REPLACE FUNCTION public.tenant_config_version(p_tenant uuid DEFAULT NULL::uuid)
|
|
50
|
+
RETURNS timestamptz
|
|
51
|
+
LANGUAGE sql
|
|
52
|
+
STABLE SECURITY DEFINER
|
|
53
|
+
SET search_path TO ''
|
|
54
|
+
AS $function$
|
|
55
|
+
WITH t AS (SELECT coalesce(p_tenant, app.current_tenant_id()) AS id)
|
|
56
|
+
SELECT greatest(
|
|
57
|
+
-- Quais módulos e quais partes deles estão ligados.
|
|
58
|
+
coalesce((SELECT max(tp.updated_at) FROM app.tenant_plugins tp, t WHERE tp.tenant_id = t.id), 'epoch'::timestamptz),
|
|
59
|
+
-- A composição do menu deste tenant.
|
|
60
|
+
coalesce((SELECT max(tn.updated_at) FROM app.tenant_nav tn, t WHERE tn.tenant_id = t.id), 'epoch'::timestamptz),
|
|
61
|
+
-- Marca, locale, fuso, moeda e toda chave registrada: tudo mora em
|
|
62
|
+
-- `tenants.settings`, e `app_config()` devolve os dois.
|
|
63
|
+
coalesce((SELECT te.updated_at FROM public.tenants te, t WHERE te.id = t.id), 'epoch'::timestamptz)
|
|
64
|
+
);
|
|
65
|
+
$function$;
|
|
66
|
+
|
|
67
|
+
COMMENT ON FUNCTION public.tenant_config_version(uuid) IS
|
|
68
|
+
'Um timestamp que se move quando QUALQUER parte de app_config() se move — ativação, menu ou settings (incluindo a marca). O cliente consulta este número; só relê a configuração inteira quando ele anda.';
|
|
69
|
+
|
|
70
|
+
-- ─────────────────────────────────────────────────────────────────────────
|
|
71
|
+
-- §2 A chave do menu é a rota
|
|
72
|
+
-- ─────────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
COMMENT ON COLUMN app.tenant_nav.entry_key IS
|
|
75
|
+
'A chave estável da entrada: a ROTA (`/financial`, `/orders/new`), que é como o shell compõe e o que o usuário vê na URL. Não `<plugin>:<route>`: três dos quatro apps desenham o rail em `pages`, e essas linhas não têm plugin nenhum para nomear.';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 072_o_shell_tambem_e_configuracao_do_tenant.sql — o banco passa a saber COMO
|
|
3
|
+
-- o aplicativo se desenha, não só o que ele tem (#280, ADR 0023).
|
|
4
|
+
--
|
|
5
|
+
-- ── O que faltava ──────────────────────────────────────────────────────────
|
|
6
|
+
-- Depois da 150 o banco sabe QUAIS módulos a conta tem (`app.tenant_plugins`) e
|
|
7
|
+
-- QUAL menu ela compôs (`app.tenant_nav`). O que continuava morando só no
|
|
8
|
+
-- repositório do app é a moldura: rail ou barra no topo, abas ou sub-rail
|
|
9
|
+
-- dentro do módulo, moldura no conteúdo, barra inferior no telefone, e o modo
|
|
10
|
+
-- claro/escuro de partida.
|
|
11
|
+
--
|
|
12
|
+
-- São seis decisões que o `FayzAppConfig` de cada app fixa para TODOS os
|
|
13
|
+
-- inquilinos dele. Uma rede com dez lojas e um escritório que roda o mesmo
|
|
14
|
+
-- aplicativo não tem como pedir topbar no escritório e rail na loja sem um
|
|
15
|
+
-- segundo build — que é exatamente o custo que esta onda existe para matar.
|
|
16
|
+
--
|
|
17
|
+
-- ── Por que chaves, e não uma tabela ───────────────────────────────────────
|
|
18
|
+
-- A 150 já escreveu a regra ao decidir o que NÃO reconstruir: "`tenants.settings`
|
|
19
|
+
-- com o registro em `app.tenant_setting_keys` (076) já governa marca, locale e
|
|
20
|
+
-- toda outra setting com chave". Uma `app.tenant_shell` daria colunas tipadas e
|
|
21
|
+
-- um CHECK de verdade, e em troca duplicaria um mecanismo que já tem tipo,
|
|
22
|
+
-- validação, RPC de leitura e de escrita, permissão e — depois da 071 — o
|
|
23
|
+
-- carimbo de versão. Seis linhas de registro custam nada disso.
|
|
24
|
+
--
|
|
25
|
+
-- O vocabulário fechado, que numa tabela seria CHECK, vira a descrição da
|
|
26
|
+
-- chave: `app.setting_type_ok` valida o TIPO, e o shell ignora um valor que não
|
|
27
|
+
-- conhece em vez de quebrar. Para configuração de aparência, ignorar é a
|
|
28
|
+
-- degradação certa; recusar seria deixar a conta sem moldura nenhuma.
|
|
29
|
+
--
|
|
30
|
+
-- ── reader = core ──────────────────────────────────────────────────────────
|
|
31
|
+
-- Nenhum plugin lê isto: quem lê é a casca. `core` é o mesmo leitor de
|
|
32
|
+
-- `locale`, `timezone` e `branding.*`, que são da mesma natureza.
|
|
33
|
+
--
|
|
34
|
+
-- Idempotente: `app.seed_setting_keys` é upsert por chave.
|
|
35
|
+
-- ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
SELECT app.seed_setting_keys($$[
|
|
38
|
+
{
|
|
39
|
+
"key": "shell.layout", "type": "string", "default": null,
|
|
40
|
+
"reader": "core",
|
|
41
|
+
"description": "Onde o menu principal vive: 'sidebar' (rail à esquerda), 'topbar' (barra no topo) ou 'minimal' (sem menu). Nulo = o que o aplicativo escolheu."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"key": "shell.module_nav", "type": "string", "default": null,
|
|
45
|
+
"reader": "core",
|
|
46
|
+
"description": "Como as telas de um mesmo módulo se apresentam: 'tabs' (abas no cabeçalho) ou 'sidebar' (sub-itens no rail). Nulo = o que o aplicativo escolheu."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"key": "shell.content_frame", "type": "boolean", "default": null,
|
|
50
|
+
"reader": "core",
|
|
51
|
+
"description": "Se o conteúdo desenha a própria moldura (cartão sobre o fundo) ou encosta na borda. Nulo = o que o aplicativo escolheu."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"key": "shell.bottom_nav", "type": "boolean", "default": null,
|
|
55
|
+
"reader": "core",
|
|
56
|
+
"description": "Barra de abas no rodapé do telefone. Ela é DERIVADA do menu que já passou pelo papel e pelo plano, então desligar aqui tira a barra, nunca acrescenta um destino."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"key": "shell.nav_transition", "type": "string", "default": null,
|
|
60
|
+
"reader": "core",
|
|
61
|
+
"description": "A transição entre telas ('fade', 'slide', 'none'). Nulo = o que o aplicativo escolheu."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"key": "shell.theme_mode", "type": "string", "default": null,
|
|
65
|
+
"reader": "core",
|
|
66
|
+
"description": "O modo de partida: 'light', 'dark' ou 'system'. É o PADRÃO da conta — a escolha de cada pessoa continua vencendo, porque é dela."
|
|
67
|
+
}
|
|
68
|
+
]$$::jsonb);
|
|
69
|
+
|
|
70
|
+
COMMENT ON TABLE app.tenant_setting_keys IS
|
|
71
|
+
'O registro de toda setting com chave que o produto aceita: tipo, valor padrão, quem lê (core ou plugin:<id>) e o alcance. Os VALORES moram em tenants.settings; aqui mora o contrato. Uma chave que não está aqui é recusada por tenant_setting_set.';
|