@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,159 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 074_a_marca_tem_uma_grafia_so_e_uma_porta_so.sql — as quatro grafias da marca
|
|
3
|
+
-- viram uma, e a porta para de mentir sobre a permissão que exige.
|
|
4
|
+
--
|
|
5
|
+
-- ── O que estava acontecendo ───────────────────────────────────────────────
|
|
6
|
+
-- `app_config()` devolve DOIS campos que falam da mesma coisa: `branding`
|
|
7
|
+
-- (de `tenant_branding()`, que lê o documento) e `settings` (de
|
|
8
|
+
-- `tenant_settings()`, que agrega as chaves registradas). Medido no cluster,
|
|
9
|
+
-- eles discordam:
|
|
10
|
+
--
|
|
11
|
+
-- synthetic-salon-network → settings diz "Synthetic Salon", branding diz NULL
|
|
12
|
+
-- espaco-renova-rio → os dois dizem NULL, e a tela mostra o nome certo
|
|
13
|
+
--
|
|
14
|
+
-- A causa é que a marca é escrita por `tenant_brand_patch`, que NÃO passa pelo
|
|
15
|
+
-- registro, e o registro declara quatro chaves planas (`branding.app_name`,
|
|
16
|
+
-- `.logo_url`, `.primary_color`, `.custom_domain`) que descrevem uma forma que
|
|
17
|
+
-- o produto parou de gravar na 032. O registro descreve um formato morto, e
|
|
18
|
+
-- `tenant_settings()` reporta esse formato morto como se fosse a verdade.
|
|
19
|
+
--
|
|
20
|
+
-- ── Quatro grafias, não três ───────────────────────────────────────────────
|
|
21
|
+
-- A 032 documentou três (camadas, cobra, camelo) e o leitor tolera as três. Há
|
|
22
|
+
-- uma QUARTA que ninguém previu: `branding.domain`, escrita pela migração do V1
|
|
23
|
+
-- (`tenant_domains → jsonb:branding.domain`, no allowlist). Nenhum dos três
|
|
24
|
+
-- leitores a conhece — então o domínio do Espaço Facial está gravado e
|
|
25
|
+
-- invisível para o sistema inteiro, inclusive para a loja pública que deveria
|
|
26
|
+
-- responder nele.
|
|
27
|
+
--
|
|
28
|
+
-- ── Por que a marca NÃO vira chave de registro ─────────────────────────────
|
|
29
|
+
-- A saída óbvia seria registrar as chaves em camadas. Ela não cabe, e não é só
|
|
30
|
+
-- pelo CHECK da coluna (`^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` recusa
|
|
31
|
+
-- `identity.logoUrl`): o registro existe para valores ESCALARES que se gravam
|
|
32
|
+
-- sozinhos, um `tenant_setting_set` por vez, com checagem de tipo. A marca tem
|
|
33
|
+
-- arrays (`voice.avoid`, `voice.ctas`) e uma semântica de fusão POR CAMADA que
|
|
34
|
+
-- existe de propósito — é o que impede duas pessoas editando abas diferentes de
|
|
35
|
+
-- apagarem o trabalho uma da outra.
|
|
36
|
+
--
|
|
37
|
+
-- Então a decisão é a inversa: a marca é um DOCUMENTO, com porta própria, e as
|
|
38
|
+
-- quatro chaves planas no registro é que são o defeito. Elas saem.
|
|
39
|
+
--
|
|
40
|
+
-- ── A permissão que não existe ─────────────────────────────────────────────
|
|
41
|
+
-- `tenant_brand_patch` exige `settings.edit`. Não há `settings.edit` no
|
|
42
|
+
-- catálogo — só `settings.read` e `settings.manage`. A função só funciona
|
|
43
|
+
-- porque `app.has_permission` casa o irmão `.manage` de qualquer permissão.
|
|
44
|
+
-- Funciona por acidente: no dia em que essa regra de irmão apertar, editar a
|
|
45
|
+
-- marca quebra para todo mundo, sem nada no código dizer por quê. Passa a
|
|
46
|
+
-- exigir `settings.manage`, que é o que ela já exige de fato — e é a mesma
|
|
47
|
+
-- permissão de `tenant_setting_set` e `tenant_plugin_set`, o que faz as três
|
|
48
|
+
-- portas de configuração pedirem a mesma coisa.
|
|
49
|
+
--
|
|
50
|
+
-- Idempotente: a normalização é escrita de forma que rodar duas vezes não muda
|
|
51
|
+
-- nada, porque depois da primeira não sobra grafia velha para mover.
|
|
52
|
+
-- ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
-- ─────────────────────────────────────────────────────────────────────────
|
|
55
|
+
-- §1 Uma grafia só, nos dados
|
|
56
|
+
--
|
|
57
|
+
-- Camadas ganham de plano. Só move o que ainda não tem destino em camada, para
|
|
58
|
+
-- que um tenant já normalizado (ou editado pela tela depois desta migração)
|
|
59
|
+
-- nunca tenha o valor novo sobrescrito pelo antigo.
|
|
60
|
+
-- ─────────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
UPDATE public.tenants t
|
|
63
|
+
SET settings = app.jsonb_set_path(
|
|
64
|
+
coalesce(t.settings, '{}'::jsonb),
|
|
65
|
+
ARRAY['branding'],
|
|
66
|
+
(
|
|
67
|
+
WITH b AS (SELECT coalesce(t.settings -> 'branding', '{}'::jsonb) AS d)
|
|
68
|
+
SELECT
|
|
69
|
+
-- Tira as grafias antigas depois de tê-las lido.
|
|
70
|
+
((((((((b.d
|
|
71
|
+
- 'app_name') - 'appName')
|
|
72
|
+
- 'logo_url') - 'logoUrl')
|
|
73
|
+
- 'primary_color') - 'primaryColor')
|
|
74
|
+
- 'custom_domain') - 'customDomain') - 'domain'
|
|
75
|
+
-- E escreve cada valor no lugar em camadas, quando ele existe e o
|
|
76
|
+
-- lugar em camadas ainda está vazio.
|
|
77
|
+
|| jsonb_strip_nulls(jsonb_build_object(
|
|
78
|
+
'identity', jsonb_strip_nulls(jsonb_build_object(
|
|
79
|
+
'name', coalesce(nullif(b.d #>> '{identity,name}', ''),
|
|
80
|
+
nullif(b.d ->> 'app_name', ''), nullif(b.d ->> 'appName', '')),
|
|
81
|
+
'logoUrl', coalesce(nullif(b.d #>> '{identity,logoUrl}', ''),
|
|
82
|
+
nullif(b.d ->> 'logo_url', ''), nullif(b.d ->> 'logoUrl', '')),
|
|
83
|
+
'customDomain', coalesce(nullif(b.d #>> '{identity,customDomain}', ''),
|
|
84
|
+
nullif(b.d ->> 'custom_domain', ''), nullif(b.d ->> 'customDomain', ''),
|
|
85
|
+
-- A quarta grafia, da migração do V1.
|
|
86
|
+
nullif(b.d ->> 'domain', ''))
|
|
87
|
+
))
|
|
88
|
+
))
|
|
89
|
+
-- `tokens` é escrito à parte porque `||` funde RASO: mandar
|
|
90
|
+
-- {tokens:{color:{...}}} trocaria `tokens` inteiro e levaria a
|
|
91
|
+
-- fonte junto.
|
|
92
|
+
|| CASE
|
|
93
|
+
WHEN coalesce(nullif(b.d #>> '{tokens,color,primary}', ''),
|
|
94
|
+
nullif(b.d ->> 'primary_color', ''), nullif(b.d ->> 'primaryColor', '')) IS NULL
|
|
95
|
+
THEN '{}'::jsonb
|
|
96
|
+
ELSE jsonb_build_object('tokens',
|
|
97
|
+
app.jsonb_set_path(coalesce(b.d -> 'tokens', '{}'::jsonb),
|
|
98
|
+
ARRAY['color','primary'],
|
|
99
|
+
to_jsonb(coalesce(nullif(b.d #>> '{tokens,color,primary}', ''),
|
|
100
|
+
nullif(b.d ->> 'primary_color', ''), nullif(b.d ->> 'primaryColor', '')))))
|
|
101
|
+
END
|
|
102
|
+
FROM b
|
|
103
|
+
))
|
|
104
|
+
WHERE t.settings -> 'branding' ?| ARRAY['app_name','appName','logo_url','logoUrl',
|
|
105
|
+
'primary_color','primaryColor',
|
|
106
|
+
'custom_domain','customDomain','domain'];
|
|
107
|
+
|
|
108
|
+
-- ─────────────────────────────────────────────────────────────────────────
|
|
109
|
+
-- §2 As quatro chaves planas saem do registro
|
|
110
|
+
--
|
|
111
|
+
-- Elas descrevem um formato que o produto parou de gravar, e a presença delas é
|
|
112
|
+
-- o que faz `tenant_settings()` reportar um recorte que contradiz
|
|
113
|
+
-- `tenant_branding()`. A marca continua em `app_config().branding`, que é a
|
|
114
|
+
-- resposta do documento inteiro.
|
|
115
|
+
-- ─────────────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
DELETE FROM app.tenant_setting_keys
|
|
118
|
+
WHERE key IN ('branding.app_name', 'branding.logo_url', 'branding.primary_color', 'branding.custom_domain');
|
|
119
|
+
|
|
120
|
+
-- ─────────────────────────────────────────────────────────────────────────
|
|
121
|
+
-- §3 A porta pede a permissão que existe
|
|
122
|
+
-- ─────────────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
CREATE OR REPLACE FUNCTION public.tenant_brand_patch(p_patch jsonb)
|
|
125
|
+
RETURNS jsonb
|
|
126
|
+
LANGUAGE plpgsql
|
|
127
|
+
SECURITY DEFINER
|
|
128
|
+
SET search_path TO ''
|
|
129
|
+
AS $function$
|
|
130
|
+
DECLARE v_tenant uuid := app.current_tenant_id();
|
|
131
|
+
BEGIN
|
|
132
|
+
IF v_tenant IS NULL THEN
|
|
133
|
+
RAISE EXCEPTION 'tenant_brand_patch: sem tenant na sessão' USING ERRCODE = '42501';
|
|
134
|
+
END IF;
|
|
135
|
+
-- `settings.manage`, a mesma de `tenant_setting_set` e `tenant_plugin_set`.
|
|
136
|
+
-- Antes pedia `settings.edit`, que não existe no catálogo e só passava porque
|
|
137
|
+
-- `app.has_permission` casa o irmão `.manage` — funcionava por acidente.
|
|
138
|
+
IF NOT app.has_permission('settings.manage', NULL::uuid) THEN
|
|
139
|
+
RAISE EXCEPTION 'tenant_brand_patch: exige settings.manage' USING ERRCODE = '42501';
|
|
140
|
+
END IF;
|
|
141
|
+
|
|
142
|
+
UPDATE public.tenants t
|
|
143
|
+
SET settings = jsonb_set(
|
|
144
|
+
coalesce(t.settings, '{}'::jsonb),
|
|
145
|
+
'{branding}',
|
|
146
|
+
-- `||` funde raso, e é o que se quer POR CAMADA: mandar
|
|
147
|
+
-- {"voice":{...}} troca a voz inteira e não toca em tokens. Mandar
|
|
148
|
+
-- {"tokens":{"color":{...}}} trocaria `tokens` inteiro, então a tela
|
|
149
|
+
-- manda a camada `tokens` completa quando mexe em qualquer cor.
|
|
150
|
+
coalesce(t.settings -> 'branding', '{}'::jsonb) || coalesce(p_patch, '{}'::jsonb)
|
|
151
|
+
),
|
|
152
|
+
updated_at = now()
|
|
153
|
+
WHERE t.id = v_tenant;
|
|
154
|
+
|
|
155
|
+
RETURN coalesce(app.brand_of(v_tenant), '{}'::jsonb);
|
|
156
|
+
END $function$;
|
|
157
|
+
|
|
158
|
+
COMMENT ON FUNCTION public.tenant_brand_patch(jsonb) IS
|
|
159
|
+
'A ÚNICA porta de escrita da marca. Funde por camada (identity / tokens / voice) para que duas pessoas em abas diferentes não apaguem o trabalho uma da outra. A marca é um documento, não um conjunto de chaves registradas — por isso não passa por tenant_setting_set.';
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 075_a_marca_carrega_o_tema_inteiro.sql — a marca do inquilino deixa de ser
|
|
3
|
+
-- uma cor e passa a ser o tema (#280, ADR 0023).
|
|
4
|
+
--
|
|
5
|
+
-- ── O que a tela mostrou ───────────────────────────────────────────────────
|
|
6
|
+
-- Medido no sistema que o Espaço Facial usa hoje
|
|
7
|
+
-- (https://sistema.espacofacial.com.br), lendo as variáveis da página:
|
|
8
|
+
--
|
|
9
|
+
-- --primary 31 50% 72% o bege
|
|
10
|
+
-- --background 40 33% 97% o papel
|
|
11
|
+
-- rail (elemento) #121212 preto
|
|
12
|
+
-- item ativo do rail #242424 com o texto em #DBB994
|
|
13
|
+
-- --radius 0.75rem
|
|
14
|
+
-- logo um SVG BRANCO, em .../tenant-appearance/1/logo-….svg
|
|
15
|
+
--
|
|
16
|
+
-- Nosso documento de marca carregava `tokens.color` e `tokens.font`. Com isso,
|
|
17
|
+
-- o Espaço Facial recebia o bege e herdava o rail do aplicativo: a marca
|
|
18
|
+
-- chegava pela metade, e a metade que faltava era justamente a que dá a
|
|
19
|
+
-- impressão de "é o meu sistema".
|
|
20
|
+
--
|
|
21
|
+
-- O V1 já tinha resolvido: `software_profiles.theme` guarda um tema inteiro por
|
|
22
|
+
-- perfil, com camadas `rail` / `surface` / `emphasis` / `status` / `chart` /
|
|
23
|
+
-- `radius`. Esta migração traz o mesmo recorte para o documento de marca, no
|
|
24
|
+
-- vocabulário que o `SaasTheme` do shell já consome — o que o banco grava é o
|
|
25
|
+
-- que a casca já sabe pintar, sem tradutor no meio.
|
|
26
|
+
--
|
|
27
|
+
-- ── Duas artes, não uma ────────────────────────────────────────────────────
|
|
28
|
+
-- A logo do Espaço Facial é branca. Sobre papel ela some; sobre o rail preto
|
|
29
|
+
-- ela aparece. Um contrato com um campo só obriga a escolher qual dos dois
|
|
30
|
+
-- fundos vai ficar errado, então `identity` ganha `logoDarkUrl` (fundo escuro)
|
|
31
|
+
-- ao lado de `logoUrl` (fundo claro). O ÍCONE quadrado (`markUrl`) não precisa
|
|
32
|
+
-- do par: o ladrilho que o desenha leva o próprio fundo escuro para qualquer
|
|
33
|
+
-- superfície, então a arte clara serve no rail, no popover e no seletor
|
|
34
|
+
-- pós-login. Só o wordmark tem duas versões, porque ele é desenhado direto
|
|
35
|
+
-- sobre o fundo de quem o hospeda.
|
|
36
|
+
--
|
|
37
|
+
-- ── E `app_config().branding` passa a devolver o documento ─────────────────
|
|
38
|
+
-- Ele devolvia `tenant_branding()`, que é o recorte ACHATADO de seis campos.
|
|
39
|
+
-- Enquanto isso o shell lia o tema de `tenants.settings.branding` direto pela
|
|
40
|
+
-- store da organização — dois leitores da mesma marca, com formas diferentes,
|
|
41
|
+
-- que é a mesma família de defeito que a 074 fechou. Agora os dois recebem
|
|
42
|
+
-- `app.brand_of`, o documento em camadas.
|
|
43
|
+
--
|
|
44
|
+
-- `tenant_branding()` fica, sem mudar de forma: as páginas públicas e o
|
|
45
|
+
-- switcher leem os seis campos achatados dela e não devem ser mexidos por uma
|
|
46
|
+
-- migração de tema.
|
|
47
|
+
--
|
|
48
|
+
-- Idempotente: substitui duas funções.
|
|
49
|
+
-- ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
CREATE OR REPLACE FUNCTION app.brand_of(p_tenant uuid)
|
|
52
|
+
RETURNS jsonb
|
|
53
|
+
LANGUAGE plpgsql
|
|
54
|
+
STABLE SECURITY DEFINER
|
|
55
|
+
SET search_path TO ''
|
|
56
|
+
AS $function$
|
|
57
|
+
DECLARE t record; b jsonb;
|
|
58
|
+
BEGIN
|
|
59
|
+
SELECT x.id, x.name, x.slug, x.logo_url, x.settings INTO t
|
|
60
|
+
FROM public.tenants x WHERE x.id = p_tenant;
|
|
61
|
+
IF t.id IS NULL THEN RETURN NULL; END IF;
|
|
62
|
+
|
|
63
|
+
b := coalesce(t.settings -> 'branding', '{}'::jsonb);
|
|
64
|
+
|
|
65
|
+
RETURN jsonb_strip_nulls(jsonb_build_object(
|
|
66
|
+
'identity', jsonb_build_object(
|
|
67
|
+
'name', coalesce(app.brand_token(b, '{identity,name}', 'app_name', 'appName'), t.name),
|
|
68
|
+
'tagline', app.brand_token(b, '{identity,tagline}', 'tagline', 'tagline'),
|
|
69
|
+
'mission', app.brand_token(b, '{identity,mission}', 'mission', 'mission'),
|
|
70
|
+
'logoUrl', coalesce(app.brand_token(b, '{identity,logoUrl}', 'logo_url', 'logoUrl'), t.logo_url),
|
|
71
|
+
-- A arte para fundo escuro. Sem ela o rail preto recebe uma logo clara,
|
|
72
|
+
-- ou — no caso do Espaço Facial, cuja logo é branca — recebe um retângulo
|
|
73
|
+
-- invisível sobre papel.
|
|
74
|
+
'logoDarkUrl', app.brand_token(b, '{identity,logoDarkUrl}', 'logo_dark_url', 'logoDarkUrl'),
|
|
75
|
+
-- O ícone quadrado, numa versão só: a arte CLARA. Sem ele o rail
|
|
76
|
+
-- recolhido corta o wordmark no meio e "Espaço Facial" chega na tela como
|
|
77
|
+
-- "aço". Uma versão basta porque o ladrilho que a desenha leva o próprio
|
|
78
|
+
-- fundo escuro para qualquer superfície — duas versões seriam dois
|
|
79
|
+
-- arquivos entregues pela casa, e um deles envelhecendo.
|
|
80
|
+
'markUrl', app.brand_token(b, '{identity,markUrl}', 'mark_url', 'markUrl'),
|
|
81
|
+
'faviconUrl',app.brand_token(b, '{identity,faviconUrl}', 'favicon_url', 'faviconUrl'),
|
|
82
|
+
'customDomain', app.brand_token(b, '{identity,customDomain}', 'custom_domain', 'customDomain')
|
|
83
|
+
),
|
|
84
|
+
'tokens', jsonb_strip_nulls(jsonb_build_object(
|
|
85
|
+
'color', jsonb_strip_nulls(jsonb_build_object(
|
|
86
|
+
'primary', app.brand_token(b, '{tokens,color,primary}', 'primary_color', 'primaryColor'),
|
|
87
|
+
'accent', app.brand_token(b, '{tokens,color,accent}', 'accent_color', 'accentColor')
|
|
88
|
+
)),
|
|
89
|
+
'font', jsonb_strip_nulls(jsonb_build_object(
|
|
90
|
+
'heading', app.brand_token(b, '{tokens,font,heading}', 'heading_font', 'headingFont'),
|
|
91
|
+
'body', app.brand_token(b, '{tokens,font,body}', 'body_font', 'bodyFont')
|
|
92
|
+
)),
|
|
93
|
+
-- O papel e a tinta sobre ele.
|
|
94
|
+
'surface', b #> '{tokens,surface}',
|
|
95
|
+
-- O rail. Mesmo vocabulário de `SaasTheme.sidebar`, de propósito.
|
|
96
|
+
'rail', b #> '{tokens,rail}',
|
|
97
|
+
'radius', b #>> '{tokens,radius}'
|
|
98
|
+
)),
|
|
99
|
+
'voice', jsonb_strip_nulls(jsonb_build_object(
|
|
100
|
+
'tone', app.brand_token(b, '{voice,tone}', 'tone_of_voice', 'toneOfVoice'),
|
|
101
|
+
'lexicon', b #> '{voice,lexicon}',
|
|
102
|
+
'avoid', b #> '{voice,avoid}',
|
|
103
|
+
'ctas', b #> '{voice,ctas}',
|
|
104
|
+
'hashtags', b #> '{voice,hashtags}'
|
|
105
|
+
))
|
|
106
|
+
));
|
|
107
|
+
END $function$;
|
|
108
|
+
|
|
109
|
+
COMMENT ON FUNCTION app.brand_of(uuid) IS
|
|
110
|
+
'A marca do inquilino como DOCUMENTO em três camadas: identity (quem é, com uma arte por fundo), tokens (como se parece — cor, papel, rail, raio, tipo) e voice (como fala). É o que o shell pinta e o que o agente lê. `tenant_branding()` continua devolvendo o recorte achatado de seis campos para o switcher e as páginas públicas.';
|
|
111
|
+
|
|
112
|
+
-- `app_config().branding` passa a devolver o documento, não o recorte.
|
|
113
|
+
CREATE OR REPLACE FUNCTION public.app_config()
|
|
114
|
+
RETURNS jsonb
|
|
115
|
+
LANGUAGE plpgsql
|
|
116
|
+
STABLE SECURITY DEFINER
|
|
117
|
+
SET search_path TO ''
|
|
118
|
+
AS $function$
|
|
119
|
+
DECLARE
|
|
120
|
+
v_tenant uuid := app.current_tenant_id();
|
|
121
|
+
v_staff text;
|
|
122
|
+
BEGIN
|
|
123
|
+
-- `platform` e o schema do control-admin: existe no cluster que hospeda a
|
|
124
|
+
-- plataforma e NAO na cadeia canonica, onde esta funcao e reconstruida do
|
|
125
|
+
-- zero. Chamar direto compila (o corpo de plpgsql so resolve nomes na
|
|
126
|
+
-- primeira execucao) e estoura na primeira chamada com "schema platform
|
|
127
|
+
-- does not exist" — que e o pedagio de `search_path` vazio ja pago na
|
|
128
|
+
-- 060. Perguntar antes deixa a mesma definicao servir aos dois: sem o
|
|
129
|
+
-- schema, ninguem e staff, e o control-admin simplesmente nao e oferecido.
|
|
130
|
+
IF pg_catalog.to_regproc('platform.staff_role(uuid)') IS NOT NULL THEN
|
|
131
|
+
EXECUTE 'SELECT platform.staff_role($1)' INTO v_staff USING (SELECT auth.uid());
|
|
132
|
+
END IF;
|
|
133
|
+
|
|
134
|
+
IF v_tenant IS NULL THEN
|
|
135
|
+
RETURN jsonb_build_object('tenant_id', NULL, 'plugins', '[]'::jsonb, 'nav', '[]'::jsonb, 'apps', '[]'::jsonb);
|
|
136
|
+
END IF;
|
|
137
|
+
|
|
138
|
+
RETURN jsonb_build_object(
|
|
139
|
+
'tenant_id', v_tenant,
|
|
140
|
+
-- O documento (075). Era o recorte achatado de `tenant_branding()`, e o
|
|
141
|
+
-- shell lia o tema por outro caminho — dois leitores, duas formas.
|
|
142
|
+
'branding', coalesce(app.brand_of(v_tenant), '{}'::jsonb),
|
|
143
|
+
'settings', public.tenant_settings(),
|
|
144
|
+
'plugins', coalesce((
|
|
145
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
146
|
+
'plugin', t.plugin_id,
|
|
147
|
+
'facet', nullif(t.facet, ''),
|
|
148
|
+
'status', CASE
|
|
149
|
+
WHEN t.expires_at IS NOT NULL AND t.expires_at <= now() THEN 'disabled'
|
|
150
|
+
ELSE t.status
|
|
151
|
+
END,
|
|
152
|
+
'source', t.source,
|
|
153
|
+
'config', t.config,
|
|
154
|
+
'expires_at', t.expires_at
|
|
155
|
+
) ORDER BY t.plugin_id, t.facet)
|
|
156
|
+
FROM app.tenant_plugins t
|
|
157
|
+
WHERE t.tenant_id = v_tenant
|
|
158
|
+
), '[]'::jsonb),
|
|
159
|
+
'apps', coalesce((
|
|
160
|
+
SELECT jsonb_agg(x ORDER BY x ->> 'house', x ->> 'name')
|
|
161
|
+
FROM (
|
|
162
|
+
SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon,
|
|
163
|
+
'accent_color', a.accent_color, 'url', a.url, 'house', false) AS x
|
|
164
|
+
FROM app.tenant_apps ta JOIN app.apps a ON a.id = ta.app_id
|
|
165
|
+
WHERE ta.tenant_id = v_tenant AND ta.status = 'active' AND a.active
|
|
166
|
+
UNION ALL
|
|
167
|
+
SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon,
|
|
168
|
+
'accent_color', a.accent_color, 'url', a.url, 'house', true) AS x
|
|
169
|
+
FROM app.apps a
|
|
170
|
+
WHERE v_staff IS NOT NULL AND a.id = 'control-admin' AND a.active AND a.url IS NOT NULL
|
|
171
|
+
) apps
|
|
172
|
+
), '[]'::jsonb),
|
|
173
|
+
'nav', coalesce((
|
|
174
|
+
SELECT jsonb_agg(jsonb_build_object('key', n.entry_key, 'hidden', n.hidden, 'label', n.label,
|
|
175
|
+
'section', n.section, 'position', n.position) ORDER BY n.entry_key)
|
|
176
|
+
FROM app.tenant_nav n WHERE n.tenant_id = v_tenant
|
|
177
|
+
), '[]'::jsonb)
|
|
178
|
+
);
|
|
179
|
+
END $function$;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 076_a_porta_do_dominio_aprende_a_grafia_nova.sql — conserto de um defeito que
|
|
3
|
+
-- a 074 introduziu.
|
|
4
|
+
--
|
|
5
|
+
-- A 074 normalizou as quatro grafias da marca numa só e moveu o domínio
|
|
6
|
+
-- customizado para `branding.identity.customDomain`. `app.brand_token` tolera
|
|
7
|
+
-- as três leituras, então tudo que passa por ele continuou funcionando.
|
|
8
|
+
--
|
|
9
|
+
-- `public.tenant_branding_public` NÃO passa por ele: ela procura o tenant com
|
|
10
|
+
-- um `WHERE lower(settings #>> '{branding,custom_domain}') = ...`, a grafia
|
|
11
|
+
-- plana, escrita à mão. Depois da normalização essa chave não existe mais em
|
|
12
|
+
-- conta nenhuma, e a função passou a não achar ninguém.
|
|
13
|
+
--
|
|
14
|
+
-- O que isso quebrou: **a porta do domínio**. É a função anônima que responde
|
|
15
|
+
-- "de quem é este endereço" antes de haver sessão — o que faz
|
|
16
|
+
-- `sistema.espacofacial.com.br` abrir já na conta certa, e o que dá marca à
|
|
17
|
+
-- loja pública e à tela de login. Ela falha em silêncio: o `EXCEPTION WHEN
|
|
18
|
+
-- OTHERS` devolve um objeto de nulos, que lê como "domínio não cadastrado".
|
|
19
|
+
--
|
|
20
|
+
-- Passa a procurar pelas três grafias, na mesma ordem de desempate do
|
|
21
|
+
-- `app.brand_token`: camadas primeiro, porque é para onde tudo caminhou.
|
|
22
|
+
--
|
|
23
|
+
-- Idempotente: substitui uma função.
|
|
24
|
+
-- ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
CREATE OR REPLACE FUNCTION public.tenant_branding_public(p_slug text DEFAULT NULL::text, p_domain text DEFAULT NULL::text)
|
|
27
|
+
RETURNS jsonb
|
|
28
|
+
LANGUAGE plpgsql
|
|
29
|
+
STABLE SECURITY DEFINER
|
|
30
|
+
SET search_path TO ''
|
|
31
|
+
AS $function$
|
|
32
|
+
DECLARE v_tenant uuid; v_domain text := lower(btrim(coalesce(p_domain, '')));
|
|
33
|
+
BEGIN
|
|
34
|
+
IF nullif(btrim(coalesce(p_slug, '')), '') IS NOT NULL THEN
|
|
35
|
+
SELECT t.id INTO v_tenant FROM public.tenants t WHERE t.slug = lower(btrim(p_slug));
|
|
36
|
+
ELSIF v_domain <> '' THEN
|
|
37
|
+
SELECT t.id INTO v_tenant
|
|
38
|
+
FROM public.tenants t
|
|
39
|
+
WHERE lower(coalesce(
|
|
40
|
+
-- A grafia em camadas (074), que é onde o domínio mora agora.
|
|
41
|
+
t.settings #>> '{branding,identity,customDomain}',
|
|
42
|
+
-- E as duas antigas, para um pool que ainda não normalizou.
|
|
43
|
+
t.settings #>> '{branding,custom_domain}',
|
|
44
|
+
t.settings #>> '{branding,customDomain}'
|
|
45
|
+
)) = v_domain
|
|
46
|
+
ORDER BY t.created_at
|
|
47
|
+
LIMIT 1;
|
|
48
|
+
END IF;
|
|
49
|
+
RETURN app.branding_of(v_tenant) - 'error';
|
|
50
|
+
EXCEPTION WHEN OTHERS THEN
|
|
51
|
+
RETURN jsonb_build_object('tenant_id', NULL, 'name', NULL, 'slug', NULL, 'logo_url', NULL, 'primary_color', NULL, 'custom_domain', NULL);
|
|
52
|
+
END $function$;
|
|
53
|
+
|
|
54
|
+
COMMENT ON FUNCTION public.tenant_branding_public(text, text) IS
|
|
55
|
+
'De quem é este endereço — a porta ANÔNIMA do domínio, respondida antes de haver sessão. É o que faz um domínio próprio abrir já na conta certa, e o que dá marca à loja pública e à tela de login. Aceita as três grafias do domínio; camadas primeiro.';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 077_o_aplicativo_tem_um_preset_que_a_conta_estende.sql — o rail curado de
|
|
3
|
+
-- cada Control vira dado, e a conta passa a ESTENDÊ-LO em vez de recriá-lo.
|
|
4
|
+
--
|
|
5
|
+
-- ── O defeito que isto fecha ───────────────────────────────────────────────
|
|
6
|
+
-- O aplicativo central monta o rail a partir dos plugins ativos da conta, e
|
|
7
|
+
-- para um restaurante isso deu QUINZE linhas planas: Pedidos, Agenda, Cozinha,
|
|
8
|
+
-- Salão, Estoque, CRM, Marketing, Reservas, Financeiro, Automações, Cardápio,
|
|
9
|
+
-- Comunicação, Análises… O ChefControl resolveu isso há muito tempo com SETE
|
|
10
|
+
-- rubricas e filhos dentro — "Operação" abriga Pedidos, Cozinha, Clientes e
|
|
11
|
+
-- Reservas; "Produtos" abriga Cardápio e Estoque.
|
|
12
|
+
--
|
|
13
|
+
-- Esse desenho é conhecimento de produto, validado em cliente. Jogá-lo fora e
|
|
14
|
+
-- deixar cada conta remontar a partir de plugins soltos é pedir a ela que
|
|
15
|
+
-- redescubra o que já sabíamos — e foi exatamente o que aconteceu.
|
|
16
|
+
--
|
|
17
|
+
-- ── O que o preset é, e o que ele NÃO é ────────────────────────────────────
|
|
18
|
+
-- Ele é o ponto de PARTIDA do aplicativo: a árvore do menu e a moldura do
|
|
19
|
+
-- shell, como o ofício pede. Ele não é o que a conta tem — isso é
|
|
20
|
+
-- `app.tenant_plugins`, e uma rubrica cujos filhos todos estão desligados
|
|
21
|
+
-- simplesmente não desenha.
|
|
22
|
+
--
|
|
23
|
+
-- As três camadas, e a ordem delas:
|
|
24
|
+
--
|
|
25
|
+
-- 1. `app.apps.preset` o aplicativo — a árvore curada do ofício
|
|
26
|
+
-- 2. `app.tenant_plugins` a conta — o que dela está ligado
|
|
27
|
+
-- 3. `app.tenant_nav` a conta de novo — a diferença que ela pediu
|
|
28
|
+
--
|
|
29
|
+
-- ── Por que uma coluna e não uma tabela ────────────────────────────────────
|
|
30
|
+
-- O preset é lido inteiro, sempre junto, e só o aplicativo escreve nele — não
|
|
31
|
+
-- há consulta que queira uma rubrica sozinha, nem escrita concorrente que peça
|
|
32
|
+
-- linhas. Uma tabela aqui daria um JOIN e uma montagem em árvore a cada boot
|
|
33
|
+
-- para responder o que um documento responde de uma vez.
|
|
34
|
+
--
|
|
35
|
+
-- Idempotente: uma coluna com IF NOT EXISTS e UPDATEs por id.
|
|
36
|
+
-- ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
ALTER TABLE app.apps ADD COLUMN IF NOT EXISTS preset jsonb NOT NULL DEFAULT '{}'::jsonb;
|
|
39
|
+
|
|
40
|
+
COMMENT ON COLUMN app.apps.preset IS
|
|
41
|
+
'O ponto de partida deste aplicativo: `shell` (layout, navegação de módulo, moldura) e `nav` (a árvore curada do ofício, com rubricas e filhos). A conta ESTENDE isto — o que ela tem vem de app.tenant_plugins e a diferença que ela pediu, de app.tenant_nav.';
|
|
42
|
+
|
|
43
|
+
-- ── ChefControl ────────────────────────────────────────────────────────────
|
|
44
|
+
-- Sete rubricas. A ordem é a da operação: primeiro o que acontece durante o
|
|
45
|
+
-- serviço, depois o que se olha entre um serviço e outro.
|
|
46
|
+
UPDATE app.apps SET preset = $$
|
|
47
|
+
{
|
|
48
|
+
"shell": { "layout": "sidebar", "moduleNav": "sidebar", "contentFrame": true, "navChildConnector": true },
|
|
49
|
+
"nav": [
|
|
50
|
+
{ "route": "/orders", "label": "Operação", "icon": "Utensils", "position": 10, "children": [
|
|
51
|
+
{ "route": "/orders", "label": "Pedidos", "icon": "ClipboardList" },
|
|
52
|
+
{ "route": "/kitchen", "label": "Cozinha", "icon": "ChefHat" },
|
|
53
|
+
{ "route": "/clients", "label": "Clientes", "icon": "Users" },
|
|
54
|
+
{ "route": "/reservations", "label": "Reservas", "icon": "CalendarClock" },
|
|
55
|
+
{ "route": "/tables", "label": "Mesas", "icon": "MapPin" }
|
|
56
|
+
]},
|
|
57
|
+
{ "route": "/delivery", "label": "Delivery", "icon": "Bike", "position": 15 },
|
|
58
|
+
{ "route": "/menu", "label": "Produtos", "icon": "Package", "position": 20, "children": [
|
|
59
|
+
{ "route": "/menu", "label": "Cardápio", "icon": "UtensilsCrossed" },
|
|
60
|
+
{ "route": "/inventory", "label": "Estoque", "icon": "Boxes" }
|
|
61
|
+
]},
|
|
62
|
+
{ "route": "/financial", "label": "Financeiro", "icon": "DollarSign", "position": 30 },
|
|
63
|
+
{ "route": "/workforce", "label": "RH", "icon": "UsersRound", "position": 35 },
|
|
64
|
+
{ "route": "/company", "label": "Cadastros", "icon": "Database", "position": 40, "children": [
|
|
65
|
+
{ "route": "/staff", "label": "Equipe", "icon": "UserCog" }
|
|
66
|
+
]},
|
|
67
|
+
{ "route": "/analytics", "label": "Relatórios", "icon": "BarChart3", "position": 41 }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
$$::jsonb WHERE id = 'resto';
|
|
71
|
+
|
|
72
|
+
-- ── StudioControl ──────────────────────────────────────────────────────────
|
|
73
|
+
-- O estúdio nunca desenhou rail à mão: ele ORDENA os plugins, e a agenda vem
|
|
74
|
+
-- primeiro porque o dia dele é uma agenda. O preset guarda só isso.
|
|
75
|
+
UPDATE app.apps SET preset = $$
|
|
76
|
+
{
|
|
77
|
+
"shell": { "layout": "sidebar", "moduleNav": "tabs", "contentFrame": true },
|
|
78
|
+
"order": ["agenda", "crm", "financial", "inventory", "marketing", "workforce", "reports"]
|
|
79
|
+
}
|
|
80
|
+
$$::jsonb WHERE id = 'beauty';
|
|
81
|
+
|
|
82
|
+
-- ── StoreControl ───────────────────────────────────────────────────────────
|
|
83
|
+
UPDATE app.apps SET preset = $$
|
|
84
|
+
{
|
|
85
|
+
"shell": { "layout": "sidebar", "moduleNav": "tabs", "contentFrame": true },
|
|
86
|
+
"order": ["shop", "orders", "inventory", "crm", "marketing", "financial", "reports"]
|
|
87
|
+
}
|
|
88
|
+
$$::jsonb WHERE id = 'shop';
|
|
89
|
+
|
|
90
|
+
-- ── FullControl ────────────────────────────────────────────────────────────
|
|
91
|
+
-- O ERP não tem ofício, então não tem rubrica de ofício: a ordem é a do
|
|
92
|
+
-- dinheiro e da venda.
|
|
93
|
+
UPDATE app.apps SET preset = $$
|
|
94
|
+
{
|
|
95
|
+
"shell": { "layout": "sidebar", "moduleNav": "sidebar", "contentFrame": true, "navChildConnector": true },
|
|
96
|
+
"order": ["crm", "conversations", "financial", "inventory", "marketing", "automations", "reports"]
|
|
97
|
+
}
|
|
98
|
+
$$::jsonb WHERE id = 'full';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 078_o_sublinhado_tambem_e_nome_de_modulo.sql — o id de plugin passa a aceitar
|
|
3
|
+
-- a mesma grafia que a facet ao lado dele já aceita, e o preset volta pro arquivo.
|
|
4
|
+
-- ---------------------------------------------------------------------------
|
|
5
|
+
-- `app.tenant_plugins` recusava `custom_forms`:
|
|
6
|
+
--
|
|
7
|
+
-- new row for relation "tenant_plugins" violates check constraint
|
|
8
|
+
-- "tenant_plugins_plugin_id_check"
|
|
9
|
+
--
|
|
10
|
+
-- Não é política, é divergência. Na MESMA tabela, uma linha acima, `facet` já
|
|
11
|
+
-- aceita sublinhado (`^[a-z][a-z0-9_-]*$`), e `tenant_setting_keys.reader`
|
|
12
|
+
-- aceita `plugin:[a-z][a-z0-9_-]*` — quer dizer: o banco já sabia escrever
|
|
13
|
+
-- `plugin:custom_forms` num lugar e recusava `custom_forms` no outro.
|
|
14
|
+
--
|
|
15
|
+
-- Das 29 fábricas de plugin publicadas hoje, `custom_forms` é a ÚNICA com
|
|
16
|
+
-- sublinhado. Duas saídas existiam:
|
|
17
|
+
--
|
|
18
|
+
-- renomear pro hífen → 44 arquivos, mais as linhas já gravadas em
|
|
19
|
+
-- `app.permissions`, `role_permissions` e nos ids de
|
|
20
|
+
-- ferramenta do agente (`custom_forms.list-templates`)
|
|
21
|
+
-- alargar a checagem → três linhas, e o id volta a ser o do manifesto
|
|
22
|
+
--
|
|
23
|
+
-- A segunda, porque o defeito é da checagem: ela nunca encontrou o catálogo que
|
|
24
|
+
-- existe. Alargar não abre espaço pra lixo — a forma continua fechada e
|
|
25
|
+
-- continua minúscula; só reconhece o separador que o resto do schema já usa.
|
|
26
|
+
--
|
|
27
|
+
-- O preço honesto: `custom-forms` e `custom_forms` passam a ser dois ids
|
|
28
|
+
-- válidos e DIFERENTES. Quem escreve é `tenant_plugin_set`, e o que ela grava é
|
|
29
|
+
-- o id do manifesto — não há duas grafias em circulação, há uma, e agora ela
|
|
30
|
+
-- cabe.
|
|
31
|
+
-- ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
ALTER TABLE app.tenant_plugins DROP CONSTRAINT IF EXISTS tenant_plugins_plugin_id_check;
|
|
34
|
+
ALTER TABLE app.plan_grants DROP CONSTRAINT IF EXISTS plan_grants_plugin_id_check;
|
|
35
|
+
ALTER TABLE app.vertical_defaults DROP CONSTRAINT IF EXISTS vertical_defaults_plugin_id_check;
|
|
36
|
+
|
|
37
|
+
ALTER TABLE app.tenant_plugins
|
|
38
|
+
ADD CONSTRAINT tenant_plugins_plugin_id_check CHECK (plugin_id ~ '^[a-z][a-z0-9_-]*$');
|
|
39
|
+
ALTER TABLE app.plan_grants
|
|
40
|
+
ADD CONSTRAINT plan_grants_plugin_id_check CHECK (plugin_id ~ '^[a-z][a-z0-9_-]*$');
|
|
41
|
+
ALTER TABLE app.vertical_defaults
|
|
42
|
+
ADD CONSTRAINT vertical_defaults_plugin_id_check CHECK (plugin_id ~ '^[a-z][a-z0-9_-]*$');
|
|
43
|
+
|
|
44
|
+
-- ── O preset volta pro arquivo ─────────────────────────────────────────────
|
|
45
|
+
-- A 077 criou `app.apps.preset` e semeou os quatro aplicativos, mas a porta que
|
|
46
|
+
-- ENTREGA o preset foi aplicada só no cluster — o arquivo nunca a escreveu. Um
|
|
47
|
+
-- banco novo ganharia a coluna e nenhum cliente a leria. Repetida aqui inteira,
|
|
48
|
+
-- que é como uma função volta a ter dono.
|
|
49
|
+
CREATE OR REPLACE FUNCTION public.app_config()
|
|
50
|
+
RETURNS jsonb
|
|
51
|
+
LANGUAGE plpgsql
|
|
52
|
+
STABLE SECURITY DEFINER
|
|
53
|
+
SET search_path TO ''
|
|
54
|
+
AS $function$
|
|
55
|
+
DECLARE
|
|
56
|
+
v_tenant uuid := app.current_tenant_id();
|
|
57
|
+
v_staff text;
|
|
58
|
+
BEGIN
|
|
59
|
+
-- `platform` e o schema do control-admin: existe no cluster que hospeda a
|
|
60
|
+
-- plataforma e NAO na cadeia canonica, onde esta funcao e reconstruida do
|
|
61
|
+
-- zero. Chamar direto compila (o corpo de plpgsql so resolve nomes na
|
|
62
|
+
-- primeira execucao) e estoura na primeira chamada com "schema platform
|
|
63
|
+
-- does not exist" — que e o pedagio de `search_path` vazio ja pago na
|
|
64
|
+
-- 060. Perguntar antes deixa a mesma definicao servir aos dois: sem o
|
|
65
|
+
-- schema, ninguem e staff, e o control-admin simplesmente nao e oferecido.
|
|
66
|
+
IF pg_catalog.to_regproc('platform.staff_role(uuid)') IS NOT NULL THEN
|
|
67
|
+
EXECUTE 'SELECT platform.staff_role($1)' INTO v_staff USING (SELECT auth.uid());
|
|
68
|
+
END IF;
|
|
69
|
+
|
|
70
|
+
IF v_tenant IS NULL THEN
|
|
71
|
+
RETURN jsonb_build_object('tenant_id', NULL, 'plugins', '[]'::jsonb, 'nav', '[]'::jsonb, 'apps', '[]'::jsonb);
|
|
72
|
+
END IF;
|
|
73
|
+
|
|
74
|
+
RETURN jsonb_build_object(
|
|
75
|
+
'tenant_id', v_tenant,
|
|
76
|
+
'branding', coalesce(app.brand_of(v_tenant), '{}'::jsonb),
|
|
77
|
+
'settings', public.tenant_settings(),
|
|
78
|
+
-- Todas as linhas, inclusive as desligadas: a regra no cliente é aditiva, e
|
|
79
|
+
-- uma linha ausente cai no padrão do manifesto — que é LIGADO.
|
|
80
|
+
'plugins', coalesce((
|
|
81
|
+
SELECT jsonb_agg(jsonb_build_object('plugin', t.plugin_id, 'facet', nullif(t.facet, ''),
|
|
82
|
+
'status', CASE WHEN t.expires_at IS NOT NULL AND t.expires_at <= now() THEN 'disabled' ELSE t.status END,
|
|
83
|
+
'source', t.source, 'config', t.config, 'expires_at', t.expires_at)
|
|
84
|
+
ORDER BY t.plugin_id, t.facet)
|
|
85
|
+
FROM app.tenant_plugins t WHERE t.tenant_id = v_tenant), '[]'::jsonb),
|
|
86
|
+
'apps', coalesce((
|
|
87
|
+
SELECT jsonb_agg(x ORDER BY x ->> 'house', x ->> 'name') FROM (
|
|
88
|
+
SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
|
|
89
|
+
'url', a.url, 'house', false, 'preset', a.preset) AS x
|
|
90
|
+
FROM app.tenant_apps ta JOIN app.apps a ON a.id = ta.app_id
|
|
91
|
+
WHERE ta.tenant_id = v_tenant AND ta.status = 'active' AND a.active
|
|
92
|
+
UNION ALL
|
|
93
|
+
SELECT jsonb_build_object('id', a.id, 'name', a.name, 'icon', a.icon, 'accent_color', a.accent_color,
|
|
94
|
+
'url', a.url, 'house', true, 'preset', a.preset) AS x
|
|
95
|
+
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),
|
|
96
|
+
'nav', coalesce((
|
|
97
|
+
SELECT jsonb_agg(jsonb_build_object('key', n.entry_key, 'hidden', n.hidden, 'label', n.label,
|
|
98
|
+
'section', n.section, 'position', n.position) ORDER BY n.entry_key)
|
|
99
|
+
FROM app.tenant_nav n WHERE n.tenant_id = v_tenant), '[]'::jsonb));
|
|
100
|
+
END $function$;
|