@fayz-ai/db 0.13.0 → 0.15.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/canonical-schema.json +106 -23
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/schema/spine.d.ts +42 -37
- package/dist/schema/spine.d.ts.map +1 -1
- package/migrations/000_baseline.sql +29667 -0
- package/migrations/001_the_tenant_has_apps.sql +314 -0
- package/migrations/002_the_spine_drops_the_vendor_and_the_dead.sql +129 -0
- package/migrations/003_the_role_rpcs_finish_and_the_mirrors_go.sql +135 -0
- package/migrations/004_the_projections_get_a_successor.sql +312 -0
- package/migrations/005_the_archetypes_anchor_to_the_unit.sql +90 -0
- package/migrations/006_the_helpers_read_the_source.sql +153 -0
- package/migrations/007_the_compat_machinery_leaves_with_the_tables.sql +77 -0
- package/migrations/008_the_switchers_see_across_accounts.sql +150 -0
- package/migrations/009_the_compat_aliases_nobody_adopted.sql +57 -0
- package/migrations/010_the_new_tenant_is_born_with_its_trade.sql +201 -0
- package/migrations/011_leadcontrol_joins_the_family.sql +120 -0
- package/migrations/012_chefcontrol_wears_its_own_mark.sql +28 -0
- package/migrations/013_the_workspace_switch_reaches_the_other_half.sql +156 -0
- package/migrations/014_the_chooser_can_read_what_it_offers.sql +91 -0
- package/migrations/015_the_unit_bindings_belong_to_one_workspace.sql +86 -0
- package/migrations/016_the_rule_stops_asking_every_row.sql +543 -0
- package/migrations/017_the_pin_is_checked_against_the_right_workspace.sql +139 -0
- package/migrations/018_the_bootstrap_says_what_you_reach.sql +114 -0
- package/migrations/019_distributing_one_row_stops_costing_the_table.sql +245 -0
- package/migrations/020_the_agenda_asks_where_someone_works.sql +81 -0
- package/migrations/022_the_line_says_which_document_it_belongs_to.sql +109 -0
- 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/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/package.json +1 -1
- package/migrations/0000_legacy_quarantine.sql +0 -86
- package/migrations/0000b_straggler_sweep.sql +0 -37
- package/migrations/000_core_v1_convert.sql +0 -343
- package/migrations/001_core.sql +0 -372
- package/migrations/002_rls_user_tenant_ids.sql +0 -64
- package/migrations/003_invitations.sql +0 -17
- package/migrations/004_archetypes.sql +0 -343
- package/migrations/005_locations_archetype.sql +0 -18
- package/migrations/006_archetype_rls.sql +0 -74
- package/migrations/007_archetype_grants.sql +0 -4
- package/migrations/008_grants.sql +0 -5
- package/migrations/009_anon_hardening.sql +0 -19
- package/migrations/010_migration_ledger.sql +0 -46
- package/migrations/011_anon_write_revoke.sql +0 -12
- package/migrations/012_rename_migration_ledger.sql +0 -61
- package/migrations/013_sequences_rls.sql +0 -22
- package/migrations/014_plan_entitlements.sql +0 -28
- package/migrations/015_agent_guard.sql +0 -108
- package/migrations/016_agent_guard_actor.sql +0 -102
- package/migrations/017_core_addresses.sql +0 -115
- package/migrations/018_global_search.sql +0 -482
- package/migrations/019_tenant_members_person_id.sql +0 -20
- package/migrations/020_fayz_projects.sql +0 -63
- package/migrations/021_tenant_roles.sql +0 -74
- package/migrations/022_invite_acceptance.sql +0 -91
- package/migrations/023_team_visibility.sql +0 -75
- package/migrations/024_analytics_engine.sql +0 -317
- package/migrations/025_created_by.sql +0 -75
- package/migrations/026_audit_trail.sql +0 -73
- package/migrations/027_domain_events.sql +0 -266
- package/migrations/028_tenant_scoped_token.sql +0 -127
- package/migrations/029_connections.sql +0 -186
- package/migrations/030_effect_idempotency.sql +0 -159
- package/migrations/031_sync_run_message.sql +0 -39
- package/migrations/032_connection_secrets.sql +0 -227
- package/migrations/033_sync_schedule.sql +0 -651
- package/migrations/034_custom_fields.sql +0 -55
- package/migrations/035_field_registry.sql +0 -148
- package/migrations/036_analytics_run_batch.sql +0 -84
- package/migrations/037_sync_tick_one_at_a_time.sql +0 -256
- package/migrations/038_onboarding_responses.sql +0 -103
- package/migrations/039_unit_tree.sql +0 -270
- package/migrations/040_resource_grants.sql +0 -474
- package/migrations/041_scoped_columns.sql +0 -192
- package/migrations/042_unit_scope_policies.sql +0 -145
- package/migrations/043_view_invoker.sql +0 -90
- package/migrations/044_unit_member_facts.sql +0 -47
- package/migrations/045_unit_entry.sql +0 -236
- package/migrations/046_membership_visible_to_members.sql +0 -85
- package/migrations/047_tasks.sql +0 -266
- package/migrations/048_every_login_is_a_person.sql +0 -190
- package/migrations/049_bookable_people.sql +0 -126
- package/migrations/050_authz_core.sql +0 -230
- package/migrations/051_authz_helpers.sql +0 -330
- package/migrations/052_authz_fanout.sql +0 -253
- package/migrations/053_authz_catalog_templates.sql +0 -402
- package/migrations/060_tenancy_seam.sql +0 -132
- package/migrations/061_create_tenant.sql +0 -125
- package/migrations/062_invitations.sql +0 -329
- package/migrations/063_legacy_tenancy_retired.sql +0 -568
- package/migrations/064_agent_guard_authz.sql +0 -118
- package/migrations/064b_supersede_unit_scope.sql +0 -205
- package/migrations/065_scaffold_sweep.sql +0 -139
- package/migrations/066_shadow_report.sql +0 -122
- package/migrations/070_permission_catalog.sql +0 -205
- package/migrations/071_v1_permission_map.sql +0 -266
- package/migrations/072_role_rpcs.sql +0 -126
- package/migrations/073_legacy_rbac_retire.sql +0 -471
- package/migrations/075_company_legal_entities.sql +0 -169
- package/migrations/076_tenant_settings.sql +0 -251
- package/migrations/080_people_roles.sql +0 -269
- package/migrations/081_people_identity.sql +0 -271
- package/migrations/082_people_membership_link.sql +0 -142
- package/migrations/083_people_data_migration.sql +0 -75
- package/migrations/084_people_registrations_tokens.sql +0 -282
- package/migrations/085_catalog_products.sql +0 -333
- package/migrations/086_catalog_pricing.sql +0 -194
- package/migrations/087_catalog_packages.sql +0 -134
- package/migrations/090_orders_venda.sql +0 -166
- package/migrations/091_orders_scaffold.sql +0 -92
- package/migrations/095_config_field_rules.sql +0 -261
- package/migrations/096_audit_append_only.sql +0 -256
- package/migrations/097_sequences_authorized.sql +0 -111
- package/migrations/098_holidays.sql +0 -80
- package/migrations/099_access_grants.sql +0 -293
- package/migrations/100_migration_schema.sql +0 -217
- package/migrations/101_migration_allowlist.sql +0 -202
- package/migrations/102_migration_rpcs.sql +0 -933
- package/migrations/103_migration_reconcile_ponr.sql +0 -132
- package/migrations/105_policies_to_authenticated.sql +0 -71
- package/migrations/106_function_search_path.sql +0 -55
- package/migrations/107_core_table_prefix.sql +0 -361
- package/migrations/108_authz_write_guards.sql +0 -149
- package/migrations/109_catalog_migration_promoted_columns.sql +0 -126
- package/migrations/110_people_kind_vocabulary.sql +0 -120
- package/migrations/111_migration_excluded_is_a_decision.sql +0 -259
- package/migrations/111_replacing_a_view_keeps_its_dependents.sql +0 -132
- package/migrations/112_money_precision_standard.sql +0 -132
- package/migrations/113_addresses_units_companies.sql +0 -114
- package/migrations/114_migrations_ledger_immutable.sql +0 -65
- package/migrations/115_registration_honours_field_rules.sql +0 -172
- package/migrations/116_field_rules_check_needs_a_tenant.sql +0 -72
- package/migrations/117_authz_reconciliation_schedule.sql +0 -82
- package/migrations/118_migration_catalog_writers.sql +0 -450
- package/migrations/119_migration_config_writers.sql +0 -193
- package/migrations/120_migration_facet_writer.sql +0 -139
- package/migrations/121_migration_financial_writers.sql +0 -294
- package/migrations/122_migration_inventory_writers.sql +0 -245
- package/migrations/123_migration_order_item_writer.sql +0 -144
- package/migrations/124_migration_ledger_writers.sql +0 -170
- package/migrations/125_migration_authz_people_writers.sql +0 -205
- package/migrations/126_owner_scoping_is_configuration.sql +0 -247
- package/migrations/127_scaffold_audit_hook.sql +0 -98
- package/migrations/128_shadow_tables_enforced.sql +0 -93
- package/migrations/129_order_kind_terminal_states.sql +0 -57
- package/migrations/130_every_login_is_a_person.sql +0 -196
- package/migrations/131_migration_invoice_split.sql +0 -127
- package/migrations/132_audit_prune_scheduled.sql +0 -28
- package/migrations/133_unit_is_a_session_property.sql +0 -155
- package/migrations/134_field_rules_on_read.sql +0 -341
- package/migrations/135_distribution_polarity.sql +0 -158
- package/migrations/136_unit_id_means_locality.sql +0 -358
- package/migrations/137_vendor_named_spine_objects.sql +0 -126
- package/migrations/138_grants_inherit_down_the_tree.sql +0 -178
- package/migrations/139_distributable_is_a_scaffold_option.sql +0 -109
- package/migrations/140_masking_is_a_scaffold_option.sql +0 -85
- package/migrations/141_renamed_objects_keep_a_compat_name.sql +0 -192
- package/migrations/142_a_person_has_addresses.sql +0 -212
- package/migrations/143_a_professional_has_a_rate.sql +0 -75
- package/migrations/144_a_product_can_be_sold_in_sizes.sql +0 -57
- package/migrations/145_the_app_says_which_tenant_it_is_in.sql +0 -123
- package/migrations/146_products_stock_has_one_writer.sql +0 -67
- package/migrations/147_every_archetype_has_a_kind_registry.sql +0 -388
- package/migrations/148_the_legacy_archetype_stops_being_writable.sql +0 -154
- package/migrations/149_the_event_log_gets_a_consumer.sql +0 -331
- package/migrations/150_the_app_is_configured_in_the_database.sql +0 -285
- package/migrations/151_stock_balance_is_an_archetype.sql +0 -144
- package/migrations/152_a_file_is_an_archetype.sql +0 -222
- package/migrations/153_a_plugin_calls_a_service_through_one_door.sql +0 -199
- package/migrations/154_the_plan_is_a_ceiling.sql +0 -258
- package/migrations/155_a_tenant_can_leave.sql +0 -271
- package/migrations/156_the_service_door_has_a_server_side.sql +0 -335
- package/migrations/157_nobody_creates_objects_in_public.sql +0 -74
- package/migrations/158_the_registry_owns_the_status_vocabulary.sql +0 -153
- package/migrations/159_the_plan_catalogue_gets_its_first_rows.sql +0 -132
- package/migrations/160_the_vertical_decides_what_starts_on.sql +0 -246
- package/migrations/161_a_tenant_leaves_with_its_data.sql +0 -363
- package/migrations/162_every_name_has_one_creator_for_real.sql +0 -126
- package/migrations/163_a_species_nobody_declared_goes_to_quarantine.sql +0 -184
- package/migrations/164_the_sequence_table_joins_the_register.sql +0 -26
- package/migrations/165_a_view_is_not_a_way_in.sql +0 -90
- package/migrations/166_the_platform_billing_leftovers_go.sql +0 -72
- package/migrations/167_a_view_gets_back_what_its_table_grants.sql +0 -112
- package/migrations/168_the_grant_census_stops_crying_wolf.sql +0 -33
- package/migrations/169_the_census_asks_postgres_instead_of_guessing.sql +0 -45
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 022_the_line_says_which_document_it_belongs_to.sql — `items.document_id`
|
|
3
|
+
-- deixa de ser uma conta e vira uma coluna, para que exista índice sobre ela.
|
|
4
|
+
--
|
|
5
|
+
-- O SINTOMA: `GET /rest/v1/v_orders?select=id,total,status,fulfillment_status
|
|
6
|
+
-- &id=in.(três ids)` devolvendo 500 com `57014: statement timeout`. Três
|
|
7
|
+
-- pedidos por id — a consulta mais seletiva que existe.
|
|
8
|
+
--
|
|
9
|
+
-- ── A medição ──────────────────────────────────────────────────────────────
|
|
10
|
+
-- -> Index Scan using items_tenant_id_id_key on items i
|
|
11
|
+
-- Index Cond: (tenant_id = …) ← só o tenant
|
|
12
|
+
-- Filter: … AND (COALESCE(order_id, invoice_id, fulfillment_id,
|
|
13
|
+
-- quote_id) = o.id)
|
|
14
|
+
-- Rows Removed by Filter: 11560
|
|
15
|
+
-- SubPlan 22 loops=34683 0,243ms ← RLS por linha
|
|
16
|
+
-- SubPlan 30 loops=34683 0,337ms
|
|
17
|
+
-- Execution Time: 20368 ms
|
|
18
|
+
--
|
|
19
|
+
-- 11.561 itens do tenant varridos POR PEDIDO — 34.683 voltas — cada uma pagando
|
|
20
|
+
-- duas funções de política. O `LEFT JOIN LATERAL` de `v_orders` até empurrou o
|
|
21
|
+
-- filtro para dentro; o que não havia era por onde entrar.
|
|
22
|
+
--
|
|
23
|
+
-- ── Por que não havia índice possível ──────────────────────────────────────
|
|
24
|
+
-- `v_items.document_id` era uma EXPRESSÃO:
|
|
25
|
+
--
|
|
26
|
+
-- COALESCE(order_id, invoice_id, fulfillment_id, quote_id) AS document_id
|
|
27
|
+
--
|
|
28
|
+
-- Uma linha de item aponta para o documento que a contém, e qual das quatro
|
|
29
|
+
-- colunas está preenchida depende de `parent_kind`. A view unificava as quatro
|
|
30
|
+
-- na leitura, o que é a modelagem certa — só que o filtro `document_id = X`
|
|
31
|
+
-- passava a ser um filtro sobre uma conta, e nenhum índice de coluna atende.
|
|
32
|
+
--
|
|
33
|
+
-- Um índice de EXPRESSÃO resolve com constante — medido, 0,159ms — mas não foi
|
|
34
|
+
-- usado quando o valor chega como parâmetro de um LATERAL através de duas views
|
|
35
|
+
-- empilhadas (`v_orders` → `v_item_balances` → `v_items`): o planejador deixa
|
|
36
|
+
-- de casar a expressão em algum ponto do empilhamento e cai para a varredura.
|
|
37
|
+
--
|
|
38
|
+
-- Depender de o planejador reconhecer a mesma expressão através de três níveis
|
|
39
|
+
-- de view é uma aposta, e esta é a segunda vez que ela sai cara.
|
|
40
|
+
--
|
|
41
|
+
-- ── A correção ─────────────────────────────────────────────────────────────
|
|
42
|
+
-- A conta vira coluna: `items.document_id`, GENERATED ALWAYS … STORED. O valor
|
|
43
|
+
-- é exatamente o mesmo — o Postgres é quem o mantém, e não há caminho de
|
|
44
|
+
-- escrita que possa discordar dele. Sobre uma coluna real o índice é trivial e
|
|
45
|
+
-- o planejador não precisa reconhecer nada.
|
|
46
|
+
--
|
|
47
|
+
-- `v_items` passa a ler a coluna em vez de recalcular. Mesmo nome, mesma
|
|
48
|
+
-- posição, mesmo tipo: nada a jusante percebe.
|
|
49
|
+
--
|
|
50
|
+
-- Resultado, mesma consulta: 20.368ms → 192ms, com
|
|
51
|
+
-- Index Cond: ((document_id = o.id) AND (parent_kind = 'order'))
|
|
52
|
+
-- e as políticas de `items` saindo do laço junto com as 11.560 linhas que
|
|
53
|
+
-- deixaram de ser lidas.
|
|
54
|
+
-- ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
-- A coluna. `IF NOT EXISTS` porque a migration é reaplicável; GENERATED porque
|
|
57
|
+
-- o valor não é opinião de quem escreve — é derivado, e derivar no banco impede
|
|
58
|
+
-- que uma porta de escrita nova esqueça de preencher.
|
|
59
|
+
ALTER TABLE public.items
|
|
60
|
+
ADD COLUMN IF NOT EXISTS document_id uuid
|
|
61
|
+
GENERATED ALWAYS AS (COALESCE(order_id, invoice_id, fulfillment_id, quote_id)) STORED;
|
|
62
|
+
|
|
63
|
+
COMMENT ON COLUMN public.items.document_id IS
|
|
64
|
+
'O documento que contém esta linha (#022), derivado de order_id/invoice_id/fulfillment_id/quote_id conforme parent_kind. Coluna e não expressão porque `v_orders` filtra por ela através de duas views empilhadas, e um índice de expressão deixa de ser reconhecido nesse caminho — era uma varredura de 11.561 itens por pedido.';
|
|
65
|
+
|
|
66
|
+
-- (tenant_id, document_id, parent_kind): o tenant primeiro como todo índice
|
|
67
|
+
-- daqui, o documento em seguida porque é o que a busca conhece, e o parent_kind
|
|
68
|
+
-- por último porque é o que desempata.
|
|
69
|
+
CREATE INDEX IF NOT EXISTS items_tenant_document_col_idx
|
|
70
|
+
ON public.items (tenant_id, document_id, parent_kind);
|
|
71
|
+
|
|
72
|
+
-- O índice de expressão da primeira tentativa, se ficou para trás.
|
|
73
|
+
DROP INDEX IF EXISTS public.items_tenant_document_idx;
|
|
74
|
+
|
|
75
|
+
-- A view lê a coluna. Mesmas colunas, mesma ordem, mesmos tipos.
|
|
76
|
+
-- `WITH (security_invoker = true)` explícito: `CREATE OR REPLACE VIEW` NÃO
|
|
77
|
+
-- herda as opções da view que substitui, e sem isto ela volta a rodar com os
|
|
78
|
+
-- direitos do dono — as políticas de quem lê deixam de valer. É o que a
|
|
79
|
+
-- asserção N16 da postura grada, e foi ela que pegou.
|
|
80
|
+
CREATE OR REPLACE VIEW public.v_items WITH (security_invoker = true) AS
|
|
81
|
+
SELECT id,
|
|
82
|
+
tenant_id,
|
|
83
|
+
unit_id,
|
|
84
|
+
parent_kind,
|
|
85
|
+
document_id,
|
|
86
|
+
order_id,
|
|
87
|
+
invoice_id,
|
|
88
|
+
fulfillment_id,
|
|
89
|
+
quote_id,
|
|
90
|
+
product_id,
|
|
91
|
+
variant_id,
|
|
92
|
+
-- `service_id` NÃO entra: ele existe no cluster onde esta migration foi
|
|
93
|
+
-- escrita e não na corrente canônica, então redeclarar a view com ele
|
|
94
|
+
-- quebrava o `apply` com "column service_id does not exist". A view
|
|
95
|
+
-- reproduz o conjunto de colunas do baseline, mais `document_id`.
|
|
96
|
+
name,
|
|
97
|
+
description,
|
|
98
|
+
quantity,
|
|
99
|
+
unit_price,
|
|
100
|
+
discount,
|
|
101
|
+
total,
|
|
102
|
+
sort_order,
|
|
103
|
+
metadata,
|
|
104
|
+
custom_fields,
|
|
105
|
+
created_at,
|
|
106
|
+
updated_at
|
|
107
|
+
FROM items i;
|
|
108
|
+
|
|
109
|
+
ANALYZE public.items;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
-- 029_o_administrador_enxerga_o_que_existe.sql
|
|
2
|
+
--
|
|
3
|
+
-- O rail do LeadControl abria com duas linhas — Painel e Análises — enquanto o
|
|
4
|
+
-- painel logo abaixo mostrava "Total de Leads: 20". Os dados estavam lá, os
|
|
5
|
+
-- plugins montados, o console limpo. Só o MENU estava vazio.
|
|
6
|
+
--
|
|
7
|
+
-- A causa não é do app. É esta assimetria, no `app.seed_role_template`:
|
|
8
|
+
--
|
|
9
|
+
-- -- o Owner recebe o CATÁLOGO, e o catálogo cresce sozinho
|
|
10
|
+
-- INSERT INTO app.role_permissions
|
|
11
|
+
-- SELECT p_tenant, v_owner, p.key FROM app.permissions p;
|
|
12
|
+
--
|
|
13
|
+
-- -- todos os outros recebem uma LISTA, escrita à mão, que não cresce
|
|
14
|
+
-- INSERT INTO app.role_permissions
|
|
15
|
+
-- SELECT p_tenant, r.id, t.permission FROM app.role_templates t ...
|
|
16
|
+
--
|
|
17
|
+
-- Um plugin publicado depois que aquela lista foi escrita registra as suas
|
|
18
|
+
-- permissões em `app.permissions` e, a partir daí, chega ao Owner de graça e
|
|
19
|
+
-- **a mais ninguém, nunca**. Não há aviso: `AdminShell` esconde a linha do rail
|
|
20
|
+
-- quando a negação é por PAPEL (negação por PLANO manteria a linha com a coroa),
|
|
21
|
+
-- então o sintoma é um menu curto, que parece escolha de produto.
|
|
22
|
+
--
|
|
23
|
+
-- Medido no cluster antes desta migration, em 10 tenants:
|
|
24
|
+
--
|
|
25
|
+
-- Administrator/Manager que enxergam crm.* 0 de 10
|
|
26
|
+
-- Administrator/Manager que enxergam conversations.* 0 de 10
|
|
27
|
+
--
|
|
28
|
+
-- E não são só esses dois. Comparando Owner com Administrator num tenant real,
|
|
29
|
+
-- faltavam ao Administrator DEZENOVE features: appointments, automations, blog,
|
|
30
|
+
-- config, conversations, courses, crm, custom_forms, dashboard, marketing,
|
|
31
|
+
-- menu, reputation, sales, scribe, shop, sites, tables e mais. Ou seja: quase
|
|
32
|
+
-- todo plugin que o SDK ganhou desde que a lista foi escrita.
|
|
33
|
+
--
|
|
34
|
+
-- Por isso a correção NÃO é acrescentar quatro linhas à lista. Acrescentar
|
|
35
|
+
-- quatro linhas conserta a queixa de hoje e reprograma a mesma queixa para o
|
|
36
|
+
-- próximo plugin. A correção é tirar o Administrador da lista e colocá-lo no
|
|
37
|
+
-- CATÁLOGO, como o Owner — menos o que é do dono.
|
|
38
|
+
--
|
|
39
|
+
-- O que continua sendo lista, de propósito: manager, staff e viewer. Esses são
|
|
40
|
+
-- recortes deliberados — um "Viewer" que enxerga tudo o que existe deixou de
|
|
41
|
+
-- ser viewer. Eles vão continuar precisando de decisão a cada plugin novo; a
|
|
42
|
+
-- diferença é que agora ALGUÉM abaixo do dono enxerga o plugin no dia em que
|
|
43
|
+
-- ele registra as permissões, e o buraco aparece na hora em vez de meses depois.
|
|
44
|
+
|
|
45
|
+
-- ── o que é do dono ────────────────────────────────────────────────────────
|
|
46
|
+
--
|
|
47
|
+
-- Precisa ser uma marca no catálogo, não uma lista no meio de uma função: quem
|
|
48
|
+
-- registra uma permissão nova é o plugin, e é ele que sabe se ela é do dono.
|
|
49
|
+
ALTER TABLE app.permissions
|
|
50
|
+
ADD COLUMN IF NOT EXISTS owner_only boolean DEFAULT false NOT NULL;
|
|
51
|
+
|
|
52
|
+
COMMENT ON COLUMN app.permissions.owner_only IS
|
|
53
|
+
'Permissão que só o Owner recebe. O Administrador recebe todo o resto do catálogo automaticamente (029).';
|
|
54
|
+
|
|
55
|
+
-- Assinatura, cartão e cancelamento são do dono. E `admin.*` é o painel da
|
|
56
|
+
-- PLATAFORMA — não é papel de tenant nenhum, nem do dono da loja.
|
|
57
|
+
--
|
|
58
|
+
-- `owner_only` governa o que se CONCEDE daqui para frente; não revoga o que já
|
|
59
|
+
-- foi concedido. Um Administrador que hoje tem `billing.read` porque a lista
|
|
60
|
+
-- antiga o deu continua tendo: tirar permissão de quem já trabalha com ela é
|
|
61
|
+
-- uma quebra, e esta migration veio consertar um menu vazio, não estreitar
|
|
62
|
+
-- ninguém.
|
|
63
|
+
UPDATE app.permissions SET owner_only = true
|
|
64
|
+
WHERE key ~ '^(billing|admin)\.' AND owner_only = false;
|
|
65
|
+
|
|
66
|
+
-- ── o administrador passa a ler o catálogo ─────────────────────────────────
|
|
67
|
+
CREATE OR REPLACE FUNCTION app.seed_role_template(p_tenant uuid, p_template text DEFAULT 'generic'::text) RETURNS uuid
|
|
68
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
69
|
+
SET search_path TO ''
|
|
70
|
+
AS $$
|
|
71
|
+
DECLARE v_owner uuid;
|
|
72
|
+
BEGIN
|
|
73
|
+
IF NOT EXISTS (SELECT 1 FROM app.role_templates WHERE template = p_template) THEN
|
|
74
|
+
RAISE EXCEPTION 'seed_role_template: unknown template %', p_template;
|
|
75
|
+
END IF;
|
|
76
|
+
|
|
77
|
+
INSERT INTO app.roles (tenant_id, key, name, is_system)
|
|
78
|
+
VALUES (p_tenant, 'owner', 'Owner', true)
|
|
79
|
+
ON CONFLICT (tenant_id, key) DO NOTHING;
|
|
80
|
+
SELECT id INTO v_owner FROM app.roles WHERE tenant_id = p_tenant AND key = 'owner';
|
|
81
|
+
|
|
82
|
+
INSERT INTO app.role_permissions (tenant_id, role_id, permission)
|
|
83
|
+
SELECT p_tenant, v_owner, p.key FROM app.permissions p
|
|
84
|
+
ON CONFLICT DO NOTHING;
|
|
85
|
+
|
|
86
|
+
INSERT INTO app.roles (tenant_id, key, name, is_system)
|
|
87
|
+
SELECT DISTINCT ON (t.role_key) p_tenant, t.role_key, t.role_name, true
|
|
88
|
+
FROM app.role_templates t WHERE t.template = p_template
|
|
89
|
+
ORDER BY t.role_key
|
|
90
|
+
ON CONFLICT (tenant_id, key) DO NOTHING;
|
|
91
|
+
|
|
92
|
+
INSERT INTO app.role_permissions (tenant_id, role_id, permission)
|
|
93
|
+
SELECT p_tenant, r.id, t.permission
|
|
94
|
+
FROM app.role_templates t
|
|
95
|
+
JOIN app.roles r ON r.tenant_id = p_tenant AND r.key = t.role_key
|
|
96
|
+
WHERE t.template = p_template
|
|
97
|
+
ON CONFLICT DO NOTHING;
|
|
98
|
+
|
|
99
|
+
-- A linha que faltava. O papel `admin` ganha o catálogo menos o que é do
|
|
100
|
+
-- dono, DEPOIS da lista — o que a lista já deu continua dado, e o que ela
|
|
101
|
+
-- esqueceu deixa de importar. Só age se o template realmente tiver um papel
|
|
102
|
+
-- `admin`: um template sem administrador não passa a ter um por isto.
|
|
103
|
+
INSERT INTO app.role_permissions (tenant_id, role_id, permission)
|
|
104
|
+
SELECT p_tenant, r.id, p.key
|
|
105
|
+
FROM app.roles r
|
|
106
|
+
CROSS JOIN app.permissions p
|
|
107
|
+
WHERE r.tenant_id = p_tenant AND r.key = 'admin' AND p.owner_only = false
|
|
108
|
+
ON CONFLICT DO NOTHING;
|
|
109
|
+
|
|
110
|
+
RETURN v_owner;
|
|
111
|
+
END $$;
|
|
112
|
+
|
|
113
|
+
COMMENT ON FUNCTION app.seed_role_template(uuid, text) IS
|
|
114
|
+
'Semeia os papéis do template. Owner e Administrador leem o CATÁLOGO (o admin sem o que é do dono); manager/staff/viewer seguem sendo recortes deliberados (029).';
|
|
115
|
+
|
|
116
|
+
-- ── as casas que já nasceram cegas ─────────────────────────────────────────
|
|
117
|
+
--
|
|
118
|
+
-- A função acima conserta quem nascer daqui para frente. Os dez tenants que já
|
|
119
|
+
-- existem precisam da mesma conta, aplicada agora.
|
|
120
|
+
DO $$
|
|
121
|
+
DECLARE v_admin integer; v_mgr integer;
|
|
122
|
+
BEGIN
|
|
123
|
+
INSERT INTO app.role_permissions (tenant_id, role_id, permission)
|
|
124
|
+
SELECT r.tenant_id, r.id, p.key
|
|
125
|
+
FROM app.roles r
|
|
126
|
+
CROSS JOIN app.permissions p
|
|
127
|
+
WHERE r.key = 'admin' AND p.owner_only = false
|
|
128
|
+
ON CONFLICT DO NOTHING;
|
|
129
|
+
GET DIAGNOSTICS v_admin = ROW_COUNT;
|
|
130
|
+
|
|
131
|
+
-- O gerente é recorte, não catálogo: ele opera a casa, não a administra.
|
|
132
|
+
-- Recebe ler/criar/editar do que o LeadControl põe no rail, e não recebe
|
|
133
|
+
-- apagar nem gerir — apagar conversa e gerir integração continuam acima
|
|
134
|
+
-- dele. `regexp_replace` pega o último segmento porque as chaves têm dois
|
|
135
|
+
-- formatos no catálogo: `sales.read` e `crm.leads.read`.
|
|
136
|
+
INSERT INTO app.role_permissions (tenant_id, role_id, permission)
|
|
137
|
+
SELECT r.tenant_id, r.id, p.key
|
|
138
|
+
FROM app.roles r
|
|
139
|
+
CROSS JOIN app.permissions p
|
|
140
|
+
WHERE r.key = 'manager'
|
|
141
|
+
AND p.owner_only = false
|
|
142
|
+
AND p.key ~ '^(crm|sales|conversations|marketing|automations)\.'
|
|
143
|
+
AND regexp_replace(p.key, '^.*\.', '') IN ('read', 'create', 'edit')
|
|
144
|
+
ON CONFLICT DO NOTHING;
|
|
145
|
+
GET DIAGNOSTICS v_mgr = ROW_COUNT;
|
|
146
|
+
|
|
147
|
+
RAISE NOTICE '029: % permissões devolvidas ao Administrador, % ao Gerente', v_admin, v_mgr;
|
|
148
|
+
END $$;
|
|
149
|
+
|
|
150
|
+
-- E o mesmo recorte entra no TEMPLATE do gerente, para o próximo tenant nascer
|
|
151
|
+
-- com ele. O administrador não precisa de linha aqui: ele passou a ler o
|
|
152
|
+
-- catálogo, e uma linha de template para ele seria a lista voltando pela porta
|
|
153
|
+
-- dos fundos.
|
|
154
|
+
INSERT INTO app.role_templates (template, role_key, role_name, sort_order, permission)
|
|
155
|
+
SELECT m.template, 'manager', m.role_name, m.sort_order, p.key
|
|
156
|
+
FROM (SELECT DISTINCT ON (template) template, role_name, sort_order
|
|
157
|
+
FROM app.role_templates WHERE role_key = 'manager'
|
|
158
|
+
ORDER BY template) m
|
|
159
|
+
CROSS JOIN app.permissions p
|
|
160
|
+
WHERE p.owner_only = false
|
|
161
|
+
AND p.key ~ '^(crm|sales|conversations|marketing|automations)\.'
|
|
162
|
+
AND regexp_replace(p.key, '^.*\.', '') IN ('read', 'create', 'edit')
|
|
163
|
+
-- NOT EXISTS em vez de ON CONFLICT: esta tabela é um catálogo sem chave
|
|
164
|
+
-- única declarada, e ali ON CONFLICT DO NOTHING não protege de nada.
|
|
165
|
+
AND NOT EXISTS (
|
|
166
|
+
SELECT 1 FROM app.role_templates x
|
|
167
|
+
WHERE x.template = m.template AND x.role_key = 'manager' AND x.permission = p.key);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 031_ticketcontrol_joins_the_family.sql — a casa de shows ganha uma linha.
|
|
3
|
+
--
|
|
4
|
+
-- O registro tem seis produtos: quatro TRADES (cozinha, loja, estúdio, escola),
|
|
5
|
+
-- uma FUNÇÃO que transbordou de aba para app (LeadControl, 011) e o painel da
|
|
6
|
+
-- própria casa (control-admin, 028). Este é o sétimo, e é um trade — mas um
|
|
7
|
+
-- trade que o ChefControl já toca pela metade.
|
|
8
|
+
--
|
|
9
|
+
-- ── Por que não é uma aba do ChefControl ───────────────────────────────────
|
|
10
|
+
-- Um bar que faz evento já cadastra o cardápio, o estoque e o caixa no Chef. O
|
|
11
|
+
-- que ele NÃO tem lá é o evento: a noite com data, capacidade por setor, lote
|
|
12
|
+
-- que vira lote, ingresso que precisa ser recusado na porta quando já entrou.
|
|
13
|
+
-- Isso não é uma tela a mais num app de restaurante — é outro turno, outra
|
|
14
|
+
-- equipe (guichê, portaria, promoter) e outro fechamento.
|
|
15
|
+
--
|
|
16
|
+
-- E é justamente por serem o MESMO TENANT que os dois convivem: `people`,
|
|
17
|
+
-- `orders`, `items` e o financeiro são os mesmos dos dois lados do switcher. A
|
|
18
|
+
-- pessoa que comprou o ingresso é a que abre comanda no bar, na mesma linha.
|
|
19
|
+
-- Nenhum concorrente brasileiro entrega isso porque nenhum é dono dos dois
|
|
20
|
+
-- lados: a bilheteria (Sympla, Ingresse) larga na porta, e o cashless (Zig)
|
|
21
|
+
-- começa nela.
|
|
22
|
+
--
|
|
23
|
+
-- ── `serves_verticals` fica VAZIO, de propósito ────────────────────────────
|
|
24
|
+
-- A 010 fez dessa coluna o gatilho de semeadura automática: um tenant que
|
|
25
|
+
-- declara um ramo ganha todo app que serve aquele ramo. Listar 'food' e
|
|
26
|
+
-- 'restaurant' aqui seria quase verdade — muito bar faz evento — e significaria
|
|
27
|
+
-- que TODO restaurante criado a partir de amanhã cresceria um segundo produto
|
|
28
|
+
-- no switcher que ninguém pediu e ninguém configurou. Um app que chega sem ser
|
|
29
|
+
-- convidado não é uma funcionalidade, é um chamado de suporte sobre uma tela
|
|
30
|
+
-- vazia. Este é opt-in: `tenant_app_set('ticket', ...)`.
|
|
31
|
+
--
|
|
32
|
+
-- ── `requires` nomeia cinco plugins ────────────────────────────────────────
|
|
33
|
+
-- `menu` e `inventory` são o bar; `orders` é o pedido (do ingresso E da
|
|
34
|
+
-- comanda, na mesma tabela); `financial` é o caixa onde a noite fecha;
|
|
35
|
+
-- `conversations` é por onde o público pergunta a hora de abrir. Sem esses
|
|
36
|
+
-- cinco o app abre num rail com Painel e placeholders.
|
|
37
|
+
--
|
|
38
|
+
-- O que NÃO está aqui: um plugin `events`. Ele nasce em M1 (ver
|
|
39
|
+
-- fayz-app/ticket-control/docs/ROADMAP.md) e é quando esta linha ganha o sexto
|
|
40
|
+
-- nome — a migration que criar as tabelas de evento é a mesma que atualiza este
|
|
41
|
+
-- array.
|
|
42
|
+
-- ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
-- ── §1 a linha do registro ────────────────────────────────────────────────
|
|
45
|
+
--
|
|
46
|
+
-- O nome é o nome de FORA (ADR: "Control" é a família, não o app). O rail
|
|
47
|
+
-- dentro diz só "Ticket"; a lista do switcher é de fora, então carrega inteiro.
|
|
48
|
+
--
|
|
49
|
+
-- #820AD1 e não outro roxo: a ficha É o produto num lançador, e o violeta claro
|
|
50
|
+
-- (#7C3AED) já é do StudioControl. Este é 18° adiante na roda e 23 pontos mais
|
|
51
|
+
-- escuro — lado a lado eles leem como duas cores, não como dois degraus de uma.
|
|
52
|
+
-- O dividendo prático de escolher o degrau ESCURO: branco sobre ele dá 7,2:1, e
|
|
53
|
+
-- a marca pode simplesmente ser o botão (o ouro do Chef não pode).
|
|
54
|
+
--
|
|
55
|
+
-- `url` é a origem de dev enquanto o app não é publicado, e é deliberadamente
|
|
56
|
+
-- visível: a coluna É a allow-list do handoff de sessão, então trocá-la é o
|
|
57
|
+
-- passo de deploy, não um detalhe.
|
|
58
|
+
INSERT INTO app.apps (id, name, icon, accent_color, url, requires, serves_verticals, active) VALUES
|
|
59
|
+
('ticket', 'TicketControl', 'Ticket', '#820AD1', 'http://localhost:5308',
|
|
60
|
+
ARRAY['menu', 'inventory', 'orders', 'financial', 'conversations'],
|
|
61
|
+
ARRAY[]::text[], true)
|
|
62
|
+
ON CONFLICT (id) DO UPDATE
|
|
63
|
+
SET name = EXCLUDED.name, icon = EXCLUDED.icon, accent_color = EXCLUDED.accent_color,
|
|
64
|
+
requires = EXCLUDED.requires, active = true,
|
|
65
|
+
-- A URL NÃO é sobrescrita numa reaplicação: quem publicar o app aponta a
|
|
66
|
+
-- coluna para o endereço real, e uma migration que reaplica não pode
|
|
67
|
+
-- devolver a frota para localhost. (Mesma regra da 028.)
|
|
68
|
+
url = coalesce(app.apps.url, EXCLUDED.url);
|
|
69
|
+
|
|
70
|
+
-- ── §2 quem recebe agora ──────────────────────────────────────────────────
|
|
71
|
+
--
|
|
72
|
+
-- Só as contas de creators@fayalabs.com que JÁ têm cardápio ativo — ou seja, as
|
|
73
|
+
-- casas que já operam um bar nesta plataforma, que é a evidência honesta de que
|
|
74
|
+
-- "o bar que também faz evento" é a história delas.
|
|
75
|
+
--
|
|
76
|
+
-- Restrito ao dono do dogfood de propósito, e isto é o que difere da 011: a
|
|
77
|
+
-- LeadControl estava pronta quando ganhou a linha. Este app está em M0 — o rail
|
|
78
|
+
-- é real, metade das telas ainda é placeholder que diz de qual marco é. Ligá-lo
|
|
79
|
+
-- para todo restaurante do cluster seria distribuir um produto pela metade.
|
|
80
|
+
-- Quando M2 fechar (o ingresso vira dinheiro), a regra geral entra numa
|
|
81
|
+
-- migration própria, que é onde ela pode ser revista sozinha.
|
|
82
|
+
INSERT INTO app.tenant_apps (tenant_id, app_id, status, source)
|
|
83
|
+
SELECT DISTINCT m.tenant_id, 'ticket', 'active', 'manual'
|
|
84
|
+
FROM app.memberships m
|
|
85
|
+
JOIN auth.users u ON u.id = m.user_id
|
|
86
|
+
WHERE u.email = 'creators@fayalabs.com'
|
|
87
|
+
AND m.active
|
|
88
|
+
AND EXISTS (
|
|
89
|
+
SELECT 1 FROM app.tenant_plugins tp
|
|
90
|
+
WHERE tp.tenant_id = m.tenant_id AND tp.plugin_id = 'menu'
|
|
91
|
+
AND tp.facet = '' AND tp.status = 'active'
|
|
92
|
+
)
|
|
93
|
+
ON CONFLICT (tenant_id, app_id) DO NOTHING;
|
|
94
|
+
|
|
95
|
+
-- E os plugins que ele precisa, para exatamente essas contas — senão o app abre
|
|
96
|
+
-- num rail com Painel e nada mais, porque a shell monta o rail com o que
|
|
97
|
+
-- app_config() devolve em `plugins`.
|
|
98
|
+
--
|
|
99
|
+
-- O teto do plano ganha do padrão, sempre: uma conta cujo plano não inclui o
|
|
100
|
+
-- plugin não o acende, e a semeadura não falha por isso.
|
|
101
|
+
INSERT INTO app.tenant_plugins (tenant_id, plugin_id, facet, status, source)
|
|
102
|
+
SELECT DISTINCT ta.tenant_id, r.plugin_id, '', 'active', 'default'
|
|
103
|
+
FROM app.tenant_apps ta
|
|
104
|
+
JOIN app.apps a ON a.id = ta.app_id
|
|
105
|
+
CROSS JOIN LATERAL unnest(a.requires) AS r(plugin_id)
|
|
106
|
+
WHERE ta.app_id = 'ticket'
|
|
107
|
+
AND ta.status = 'active'
|
|
108
|
+
AND public.plan_permits(r.plugin_id, '', ta.tenant_id)
|
|
109
|
+
ON CONFLICT (tenant_id, plugin_id, facet) DO NOTHING;
|