@fayz-ai/db 0.12.0 → 0.14.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 +132 -0
- 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 +116 -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/package.json +3 -2
- 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 -36
- 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 -81
- 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
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 016_the_rule_stops_asking_every_row.sql — a list of 3.545 clientes stops
|
|
3
|
+
-- costing 3.545 answers to the same question.
|
|
4
|
+
--
|
|
5
|
+
-- THE BUG, as seen: `/clients` no Espaço Facial não carrega. Não é lentidão —
|
|
6
|
+
-- é o teto de 8s do papel `authenticated` estourando, e a tela dizendo "não foi
|
|
7
|
+
-- possível carregar" sem nenhuma linha. A 3.545 clientes o tenant é o primeiro
|
|
8
|
+
-- de escala real da rede; nos outros a mesma consulta responde em milissegundos
|
|
9
|
+
-- e nada disto aparece.
|
|
10
|
+
--
|
|
11
|
+
-- O QUE O PLANEJADOR DIZ (EXPLAIN ANALYZE, 50 linhas, ordenado por nome):
|
|
12
|
+
--
|
|
13
|
+
-- Limit actual time=10034
|
|
14
|
+
-- Index Scan using people_tenant_kind rows=3545
|
|
15
|
+
-- SubPlan 1 (available_in_my_units) 2.471ms × 3545 = 8.760s
|
|
16
|
+
-- SubPlan 8 (has_permission) 0.333ms × 3545 = 1.180s
|
|
17
|
+
-- Execution Time: 10035 ms
|
|
18
|
+
--
|
|
19
|
+
-- Duas perguntas feitas UMA VEZ POR LINHA, e as duas com a mesma resposta em
|
|
20
|
+
-- todas elas.
|
|
21
|
+
--
|
|
22
|
+
-- ── A primeira: a regra de distribuição ────────────────────────────────────
|
|
23
|
+
-- `people_available_in` chama `app.available_in_my_units('people.person', id)`
|
|
24
|
+
-- para cada linha sem unidade. A função monta quatro CTEs, resolve a cadeia de
|
|
25
|
+
-- cada unidade do chamador e cruza com `app.resource_grants` — para descobrir,
|
|
26
|
+
-- 3.545 vezes seguidas, que ESTE TENANT NÃO TEM NENHUMA REGRA DE DISTRIBUIÇÃO.
|
|
27
|
+
-- São zero tuplas no banco inteiro fora uma.
|
|
28
|
+
--
|
|
29
|
+
-- E quando não há tupla, a resposta é conhecida sem olhar linha nenhuma. Com
|
|
30
|
+
-- `tup` vazio o corpo da função reduz a:
|
|
31
|
+
--
|
|
32
|
+
-- NOT EXISTS(direct unavailable_in) -- vazio ⇒ true
|
|
33
|
+
-- AND ( EXISTS(direct available_in) -- vazio ⇒ false
|
|
34
|
+
-- OR EXISTS(verdicts allow) -- 'none' ⇒ false
|
|
35
|
+
-- OR ( NOT EXISTS(tup available_in) -- vazio ⇒ true
|
|
36
|
+
-- AND (EXISTS(verdicts 'none') OR NOT EXISTS(mine)) ) )
|
|
37
|
+
--
|
|
38
|
+
-- O último parêntese é verdade nos dois casos possíveis: com unidades, todo
|
|
39
|
+
-- veredito é 'none'; sem unidades, `NOT EXISTS(mine)`. Logo: sem tupla do tipo
|
|
40
|
+
-- no tenant, a função devolve `true` para TODA linha. Isso não é uma
|
|
41
|
+
-- aproximação — é o mesmo valor, e é por isso que dá para perguntar uma vez.
|
|
42
|
+
--
|
|
43
|
+
-- A pergunta barata — "este tenant tem alguma regra deste tipo?" — não depende
|
|
44
|
+
-- da linha, então o planejador a promove a InitPlan e ela roda UMA vez. Só
|
|
45
|
+
-- quando a resposta é sim é que a função cara volta a ser chamada, e aí ela é o
|
|
46
|
+
-- preço correto de um recurso que realmente foi distribuído.
|
|
47
|
+
--
|
|
48
|
+
-- ── A segunda: a permissão ─────────────────────────────────────────────────
|
|
49
|
+
-- `(select app.has_permission('people.read', unit_id))` correlaciona pela
|
|
50
|
+
-- coluna, então roda por linha — inclusive nas linhas em que `unit_id` é NULL,
|
|
51
|
+
-- onde `effective_role_id` ignora a unidade e a resposta é sempre a mesma.
|
|
52
|
+
--
|
|
53
|
+
-- Separar os dois casos num CASE resolve sem mudar o resultado: o ramo do NULL
|
|
54
|
+
-- não menciona coluna nenhuma, vira InitPlan, e roda uma vez para a consulta
|
|
55
|
+
-- inteira. O ramo da unidade continua exatamente como era. Verificado no
|
|
56
|
+
-- planejador: `Filter: CASE WHEN (unit_id IS NULL) THEN (InitPlan 1).col1 ELSE
|
|
57
|
+
-- (SubPlan 2) END`, com o SubPlan `never executed`.
|
|
58
|
+
--
|
|
59
|
+
-- ── E a terceira, que ainda não doeu ───────────────────────────────────────
|
|
60
|
+
-- `app.has_grant(tipo, id, 'shared_with')` no predicado de escopo é a mesma
|
|
61
|
+
-- armadilha, só que adormecida: ela só não custa hoje porque `owner_id IS NULL`
|
|
62
|
+
-- curto-circuita o OR antes dela. Numa tabela com dono preenchido ela roda por
|
|
63
|
+
-- linha, com a mesma resposta constante. Ganha a mesma guarda, pelo mesmo
|
|
64
|
+
-- motivo, antes de virar o próximo incidente.
|
|
65
|
+
--
|
|
66
|
+
-- ── O que muda de verdade ──────────────────────────────────────────────────
|
|
67
|
+
-- Nada no que cada política DECIDE. O que muda é onde a decisão é tomada: o
|
|
68
|
+
-- que não depende da linha sai de dentro do laço. As políticas são geradas —
|
|
69
|
+
-- `scaffold_table`, `scaffold_refresh_policies`, `scaffold_distributable`,
|
|
70
|
+
-- `masked_view_refresh` — então a correção mora nos geradores, e o bloco final
|
|
71
|
+
-- os re-executa para todas as tabelas do registro.
|
|
72
|
+
-- ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
-- ---------------------------------------------------------------------------
|
|
75
|
+
-- 1. A pergunta barata: existe ALGUMA tupla deste tipo neste tenant?
|
|
76
|
+
-- Não recebe id de linha de propósito — é o que a deixa sair do laço.
|
|
77
|
+
-- ---------------------------------------------------------------------------
|
|
78
|
+
CREATE OR REPLACE FUNCTION app.grants_exist(p_type text, p_relations text[])
|
|
79
|
+
RETURNS boolean
|
|
80
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
81
|
+
SET search_path TO ''
|
|
82
|
+
AS $$
|
|
83
|
+
SELECT EXISTS (
|
|
84
|
+
SELECT 1 FROM app.resource_grants g
|
|
85
|
+
WHERE g.tenant_id = app.current_tenant_id()
|
|
86
|
+
AND g.resource_type = p_type
|
|
87
|
+
AND g.relation = ANY (p_relations)
|
|
88
|
+
);
|
|
89
|
+
$$;
|
|
90
|
+
|
|
91
|
+
REVOKE ALL ON FUNCTION app.grants_exist(text, text[]) FROM PUBLIC;
|
|
92
|
+
GRANT ALL ON FUNCTION app.grants_exist(text, text[]) TO authenticated;
|
|
93
|
+
GRANT ALL ON FUNCTION app.grants_exist(text, text[]) TO service_role;
|
|
94
|
+
|
|
95
|
+
COMMENT ON FUNCTION app.grants_exist(text, text[]) IS
|
|
96
|
+
'Este tenant tem alguma tupla destas relações para este tipo de recurso (#016)? Não recebe id de linha porque a pergunta não é sobre a linha — é o que permite ao planejador promovê-la a InitPlan e tirar a regra cara de dentro do laço da RLS.';
|
|
97
|
+
|
|
98
|
+
-- ---------------------------------------------------------------------------
|
|
99
|
+
-- 2. A mesma saída antecipada DENTRO da função, para quem a chama direto
|
|
100
|
+
-- (RPCs, views, `excluded_in_my_units`) e não passa por política nenhuma.
|
|
101
|
+
-- Corpo idêntico ao da 000 depois da guarda.
|
|
102
|
+
-- ---------------------------------------------------------------------------
|
|
103
|
+
CREATE OR REPLACE FUNCTION app.available_in_my_units(p_type text, p_id uuid)
|
|
104
|
+
RETURNS boolean
|
|
105
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
106
|
+
SET search_path TO ''
|
|
107
|
+
AS $$
|
|
108
|
+
-- Sem nenhuma tupla deste tipo no tenant não há o que resolver: a derivação
|
|
109
|
+
-- está no cabeçalho da 016 e termina em `true` para toda linha. Um CASE e não
|
|
110
|
+
-- um OR porque o ramo do CASE é avaliado sob demanda de forma garantida — as
|
|
111
|
+
-- CTEs abaixo nem chegam a ser montadas. A guarda é uma sondagem de índice no
|
|
112
|
+
-- prefixo (tenant_id, resource_type) da PK de resource_grants.
|
|
113
|
+
SELECT CASE
|
|
114
|
+
WHEN NOT app.grants_exist(p_type, ARRAY['available_in', 'unavailable_in']) THEN true
|
|
115
|
+
ELSE (
|
|
116
|
+
WITH me AS (
|
|
117
|
+
SELECT app.current_tenant_id() AS tenant_id,
|
|
118
|
+
auth.uid() AS uid,
|
|
119
|
+
nullif(app.current_unit_id(), '00000000-0000-0000-0000-000000000000'::uuid) AS focus
|
|
120
|
+
),
|
|
121
|
+
tup AS (
|
|
122
|
+
SELECT g.relation, g.subject_type, g.subject_id
|
|
123
|
+
FROM app.resource_grants g, me
|
|
124
|
+
WHERE g.tenant_id = me.tenant_id AND g.resource_type = p_type AND g.resource_id = p_id
|
|
125
|
+
AND g.relation IN ('available_in', 'unavailable_in')
|
|
126
|
+
),
|
|
127
|
+
-- me, or a role of mine: outside the tree, so it simply decides
|
|
128
|
+
direct AS (
|
|
129
|
+
SELECT t.relation FROM tup t, me
|
|
130
|
+
WHERE (t.subject_type = 'user' AND t.subject_id = me.uid)
|
|
131
|
+
OR (t.subject_type = 'role' AND t.subject_id IN (
|
|
132
|
+
SELECT m.role_id FROM app.memberships m
|
|
133
|
+
WHERE m.tenant_id = me.tenant_id AND m.user_id = me.uid AND m.active
|
|
134
|
+
UNION
|
|
135
|
+
SELECT mu.role_override_id FROM app.membership_units mu
|
|
136
|
+
WHERE mu.tenant_id = me.tenant_id AND mu.user_id = me.uid AND mu.role_override_id IS NOT NULL))
|
|
137
|
+
),
|
|
138
|
+
mine AS (
|
|
139
|
+
SELECT a.unit_id FROM app.user_unit_access a, me
|
|
140
|
+
WHERE a.tenant_id = me.tenant_id AND a.user_id = me.uid
|
|
141
|
+
AND (me.focus IS NULL OR a.unit_id = me.focus)
|
|
142
|
+
),
|
|
143
|
+
verdicts AS (
|
|
144
|
+
SELECT app.unit_release_verdict(p_type, p_id, mine.unit_id) AS v FROM mine
|
|
145
|
+
)
|
|
146
|
+
SELECT NOT EXISTS (SELECT 1 FROM direct WHERE relation = 'unavailable_in')
|
|
147
|
+
AND (
|
|
148
|
+
EXISTS (SELECT 1 FROM direct WHERE relation = 'available_in')
|
|
149
|
+
OR EXISTS (SELECT 1 FROM verdicts WHERE v = 'allow')
|
|
150
|
+
OR (NOT EXISTS (SELECT 1 FROM tup WHERE relation = 'available_in')
|
|
151
|
+
AND (EXISTS (SELECT 1 FROM verdicts WHERE v = 'none') OR NOT EXISTS (SELECT 1 FROM mine)))
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
END;
|
|
155
|
+
$$;
|
|
156
|
+
|
|
157
|
+
-- ---------------------------------------------------------------------------
|
|
158
|
+
-- 3. Como uma checagem de permissão é RENDERIZADA num predicado de varredura.
|
|
159
|
+
-- Um lugar só, porque quatro geradores a escrevem.
|
|
160
|
+
-- ---------------------------------------------------------------------------
|
|
161
|
+
CREATE OR REPLACE FUNCTION app.scaffold_perm_sql(p_perm text, p_scope text DEFAULT NULL::text)
|
|
162
|
+
RETURNS text
|
|
163
|
+
LANGUAGE sql IMMUTABLE
|
|
164
|
+
SET search_path TO ''
|
|
165
|
+
AS $$
|
|
166
|
+
-- `unit` = a coluna é NOT NULL, então o ramo do NULL é código morto e a forma
|
|
167
|
+
-- curta é a honesta. Fora disso, o CASE separa o caso que não depende da
|
|
168
|
+
-- linha (InitPlan, uma vez) do que depende (SubPlan, como sempre foi).
|
|
169
|
+
SELECT CASE WHEN p_scope = 'unit'
|
|
170
|
+
THEN format('(select app.has_permission(%L, unit_id))', p_perm)
|
|
171
|
+
ELSE format('(CASE WHEN unit_id IS NULL'
|
|
172
|
+
|| ' THEN (select app.has_permission(%L, NULL::uuid))'
|
|
173
|
+
|| ' ELSE (select app.has_permission(%L, unit_id)) END)', p_perm, p_perm)
|
|
174
|
+
END;
|
|
175
|
+
$$;
|
|
176
|
+
|
|
177
|
+
-- O `p_scope` novo faz desta uma função NOVA — a de um argumento continua
|
|
178
|
+
-- existindo —, e função nova nasce com EXECUTE para PUBLIC, que inclui `anon`.
|
|
179
|
+
-- Nada em `app` é chamável de fora; é o que a asserção N15c da postura grada.
|
|
180
|
+
REVOKE ALL ON FUNCTION app.scaffold_perm_sql(text, text) FROM PUBLIC;
|
|
181
|
+
GRANT ALL ON FUNCTION app.scaffold_perm_sql(text, text) TO service_role;
|
|
182
|
+
|
|
183
|
+
COMMENT ON FUNCTION app.scaffold_perm_sql(text, text) IS
|
|
184
|
+
'Renderiza a checagem de permissão de um predicado de varredura (#016). O ramo `unit_id IS NULL` não menciona coluna nenhuma e por isso vira InitPlan — é o que impede uma pergunta de resposta constante de ser feita uma vez por linha.';
|
|
185
|
+
|
|
186
|
+
-- ---------------------------------------------------------------------------
|
|
187
|
+
-- 4. O predicado de escopo compartilhado por select/update/delete.
|
|
188
|
+
-- Mesmas decisões da 000; o que muda é onde elas são avaliadas.
|
|
189
|
+
-- ---------------------------------------------------------------------------
|
|
190
|
+
CREATE OR REPLACE FUNCTION app.scaffold_scope_sql(p_resource_type text, p_plugin text, p_scope text DEFAULT NULL::text)
|
|
191
|
+
RETURNS text
|
|
192
|
+
LANGUAGE sql IMMUTABLE
|
|
193
|
+
SET search_path TO ''
|
|
194
|
+
AS $$
|
|
195
|
+
SELECT 'tenant_id = (select app.current_tenant_id())'
|
|
196
|
+
|| CASE WHEN p_scope = 'unit'
|
|
197
|
+
THEN ' AND (select app.has_unit(unit_id))'
|
|
198
|
+
ELSE ' AND (unit_id IS NULL OR (select app.has_unit(unit_id)))' END
|
|
199
|
+
|| format(
|
|
200
|
+
' AND (NOT (select app.owner_scoped(%L))'
|
|
201
|
+
|| ' OR owner_id IS NULL OR owner_id = (select auth.uid())'
|
|
202
|
+
-- A guarda barata na frente da cara: sem tupla `shared_with` deste
|
|
203
|
+
-- tipo no tenant, `has_grant` só pode responder false, e agora
|
|
204
|
+
-- responde isso uma vez em vez de uma vez por linha.
|
|
205
|
+
|| ' OR ((select app.grants_exist(%L, ARRAY[''shared_with'']))'
|
|
206
|
+
|| ' AND (select app.has_grant(%L, id, ARRAY[''shared_with''])))'
|
|
207
|
+
|| ' OR %s)',
|
|
208
|
+
p_resource_type, p_resource_type, p_resource_type,
|
|
209
|
+
app.scaffold_perm_sql(p_plugin || '.read_all', p_scope));
|
|
210
|
+
$$;
|
|
211
|
+
|
|
212
|
+
-- ---------------------------------------------------------------------------
|
|
213
|
+
-- 5. Os geradores de política. Idênticos aos da 000 fora a renderização.
|
|
214
|
+
-- ---------------------------------------------------------------------------
|
|
215
|
+
CREATE OR REPLACE FUNCTION app.scaffold_refresh_policies(p_table text) RETURNS void
|
|
216
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
217
|
+
SET search_path TO ''
|
|
218
|
+
AS $$
|
|
219
|
+
DECLARE
|
|
220
|
+
v_schema text := split_part(replace(p_table, '"', ''), '.', 1);
|
|
221
|
+
v_name text := split_part(replace(p_table, '"', ''), '.', 2);
|
|
222
|
+
v_q text := format('%I.%I', split_part(replace(p_table, '"', ''), '.', 1), split_part(replace(p_table, '"', ''), '.', 2));
|
|
223
|
+
r app.scaffold_registry%ROWTYPE;
|
|
224
|
+
v_scope text;
|
|
225
|
+
v_class text;
|
|
226
|
+
BEGIN
|
|
227
|
+
SELECT * INTO r FROM app.scaffold_registry WHERE table_schema = v_schema AND table_name = v_name;
|
|
228
|
+
IF NOT FOUND THEN
|
|
229
|
+
RAISE EXCEPTION 'scaffold_refresh_policies: %.% is not in the scaffold registry', v_schema, v_name USING ERRCODE = 'P0002';
|
|
230
|
+
END IF;
|
|
231
|
+
|
|
232
|
+
-- The `unit` rendering drops the `unit_id IS NULL OR` branch, which is only
|
|
233
|
+
-- sound when the column really is NOT NULL. Declaring a table `unit` while its
|
|
234
|
+
-- writers still produce NULLs is exactly the state this file leaves most tables
|
|
235
|
+
-- in — so the optimisation follows the CONSTRAINT, never the label. Getting
|
|
236
|
+
-- this wrong hid every unit-less financial movement and took
|
|
237
|
+
-- v_financial_customer_credit to 0.00.
|
|
238
|
+
SELECT CASE WHEN c.is_nullable = 'NO' THEN 'unit' ELSE NULL END INTO v_class
|
|
239
|
+
FROM information_schema.columns c
|
|
240
|
+
WHERE c.table_schema = v_schema AND c.table_name = v_name AND c.column_name = 'unit_id';
|
|
241
|
+
v_scope := app.scaffold_scope_sql(r.resource_type, r.plugin, v_class);
|
|
242
|
+
|
|
243
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON %s', v_name || '_authz_select', v_q);
|
|
244
|
+
EXECUTE format('CREATE POLICY %I ON %s FOR SELECT TO authenticated USING (%s AND %s)',
|
|
245
|
+
v_name || '_authz_select', v_q, v_scope, app.scaffold_perm_sql(r.plugin || '.read', v_class));
|
|
246
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON %s', v_name || '_authz_update', v_q);
|
|
247
|
+
EXECUTE format('CREATE POLICY %I ON %s FOR UPDATE TO authenticated USING (%s AND %s) WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (unit_id IS NULL OR (select app.has_unit(unit_id))) AND (select app.has_permission(%L, unit_id)))',
|
|
248
|
+
v_name || '_authz_update', v_q, v_scope, app.scaffold_perm_sql(r.plugin || '.edit', v_class), r.plugin || '.edit');
|
|
249
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON %s', v_name || '_authz_delete', v_q);
|
|
250
|
+
EXECUTE format('CREATE POLICY %I ON %s FOR DELETE TO authenticated USING (%s AND %s)',
|
|
251
|
+
v_name || '_authz_delete', v_q, v_scope, app.scaffold_perm_sql(r.plugin || '.delete', v_class));
|
|
252
|
+
END $$;
|
|
253
|
+
|
|
254
|
+
CREATE OR REPLACE FUNCTION app.scaffold_distributable(p_table regclass, p_resource_type text DEFAULT NULL::text, p_key_column text DEFAULT 'id'::text, p_unit_escape boolean DEFAULT true, p_enabled boolean DEFAULT true) RETURNS void
|
|
255
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
256
|
+
SET search_path TO ''
|
|
257
|
+
AS $$
|
|
258
|
+
DECLARE
|
|
259
|
+
v_schema text; v_name text; v_q text; v_policy text;
|
|
260
|
+
r app.scaffold_registry%ROWTYPE; v_rt text;
|
|
261
|
+
BEGIN
|
|
262
|
+
SELECT n.nspname, c.relname INTO v_schema, v_name
|
|
263
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.oid = p_table;
|
|
264
|
+
v_q := format('%I.%I', v_schema, v_name);
|
|
265
|
+
v_policy := v_name || '_available_in';
|
|
266
|
+
|
|
267
|
+
SELECT * INTO r FROM app.scaffold_registry WHERE table_schema = v_schema AND table_name = v_name;
|
|
268
|
+
IF NOT FOUND THEN
|
|
269
|
+
RAISE EXCEPTION 'scaffold_distributable: %.% is not scaffolded', v_schema, v_name USING ERRCODE = 'P0002';
|
|
270
|
+
END IF;
|
|
271
|
+
v_rt := coalesce(p_resource_type, r.resource_type);
|
|
272
|
+
|
|
273
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON %s', v_policy, v_q);
|
|
274
|
+
|
|
275
|
+
IF p_enabled THEN
|
|
276
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
277
|
+
WHERE table_schema = v_schema AND table_name = v_name AND column_name = p_key_column) THEN
|
|
278
|
+
RAISE EXCEPTION 'scaffold_distributable: %.% has no column %', v_schema, v_name, p_key_column;
|
|
279
|
+
END IF;
|
|
280
|
+
-- RESTRICTIVE: permissive policies OR together, so a release rule that is
|
|
281
|
+
-- meant to REMOVE rows has to AND with the scaffold's select.
|
|
282
|
+
--
|
|
283
|
+
-- A guarda vem PRIMEIRO e sem coluna nenhuma: é o que a torna InitPlan.
|
|
284
|
+
-- Quando o tenant não distribui este tipo de recurso — o caso de todo
|
|
285
|
+
-- tenant hoje — a regra cara não é chamada nem uma vez.
|
|
286
|
+
EXECUTE format(
|
|
287
|
+
'CREATE POLICY %I ON %s AS RESTRICTIVE FOR SELECT TO authenticated USING (NOT (select app.grants_exist(%L, ARRAY[''available_in'', ''unavailable_in''])) OR %s(select app.available_in_my_units(%L, %I)))',
|
|
288
|
+
v_policy, v_q, v_rt,
|
|
289
|
+
CASE WHEN p_unit_escape THEN 'unit_id IS NOT NULL OR ' ELSE '' END,
|
|
290
|
+
v_rt, p_key_column);
|
|
291
|
+
END IF;
|
|
292
|
+
|
|
293
|
+
UPDATE app.scaffold_registry
|
|
294
|
+
SET distributable = p_enabled,
|
|
295
|
+
dist_resource_type = CASE WHEN p_enabled THEN v_rt END,
|
|
296
|
+
dist_key_column = CASE WHEN p_enabled THEN p_key_column END,
|
|
297
|
+
dist_unit_escape = CASE WHEN p_enabled THEN p_unit_escape END
|
|
298
|
+
WHERE table_schema = v_schema AND table_name = v_name;
|
|
299
|
+
END $$;
|
|
300
|
+
|
|
301
|
+
-- O gerador de tabela nova: a mesma renderização, para que uma tabela criada
|
|
302
|
+
-- depois desta migration já nasça com a forma rápida.
|
|
303
|
+
CREATE OR REPLACE FUNCTION app.scaffold_table(p_table regclass, p_resource_type text, p_plugin text, p_owner_scoping boolean DEFAULT true, p_mode text DEFAULT 'enforce'::text) RETURNS void
|
|
304
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
305
|
+
SET search_path TO ''
|
|
306
|
+
AS $$
|
|
307
|
+
DECLARE
|
|
308
|
+
v_schema text;
|
|
309
|
+
v_name text;
|
|
310
|
+
v_q text; -- quoted qualified name
|
|
311
|
+
v_prev text[] := '{}';
|
|
312
|
+
v_rows bigint;
|
|
313
|
+
r record;
|
|
314
|
+
v_scope text; -- the row-scope predicate shared by every policy
|
|
315
|
+
BEGIN
|
|
316
|
+
IF p_mode NOT IN ('shadow', 'enforce') THEN
|
|
317
|
+
RAISE EXCEPTION 'scaffold_table: mode must be shadow|enforce';
|
|
318
|
+
END IF;
|
|
319
|
+
SELECT n.nspname, c.relname INTO v_schema, v_name
|
|
320
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.oid = p_table;
|
|
321
|
+
v_q := format('%I.%I', v_schema, v_name);
|
|
322
|
+
|
|
323
|
+
-- ── columns
|
|
324
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = v_schema AND table_name = v_name AND column_name = 'id') THEN
|
|
325
|
+
RAISE EXCEPTION 'scaffold_table: % has no id column', v_q;
|
|
326
|
+
END IF;
|
|
327
|
+
EXECUTE format('SELECT count(*) FROM %s', v_q) INTO v_rows;
|
|
328
|
+
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = v_schema AND table_name = v_name AND column_name = 'tenant_id') THEN
|
|
329
|
+
EXECUTE format('ALTER TABLE %s ADD COLUMN tenant_id uuid REFERENCES public.tenants(id) ON DELETE CASCADE', v_q);
|
|
330
|
+
IF v_rows = 0 THEN EXECUTE format('ALTER TABLE %s ALTER COLUMN tenant_id SET NOT NULL', v_q); END IF;
|
|
331
|
+
END IF;
|
|
332
|
+
EXECUTE format('ALTER TABLE %s ADD COLUMN IF NOT EXISTS unit_id uuid', v_q);
|
|
333
|
+
EXECUTE format('ALTER TABLE %s ADD COLUMN IF NOT EXISTS owner_id uuid', v_q);
|
|
334
|
+
EXECUTE format('ALTER TABLE %s ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now()', v_q);
|
|
335
|
+
EXECUTE format('ALTER TABLE %s ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now()', v_q);
|
|
336
|
+
|
|
337
|
+
-- ── unique(tenant_id, id) — enables composite FKs from other tables
|
|
338
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = p_table AND conname = v_name || '_tenant_id_id_key') THEN
|
|
339
|
+
EXECUTE format('ALTER TABLE %s ADD CONSTRAINT %I UNIQUE (tenant_id, id)', v_q, v_name || '_tenant_id_id_key');
|
|
340
|
+
END IF;
|
|
341
|
+
-- ── composite FKs: unit within tenant, owner a member of the tenant
|
|
342
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = p_table AND conname = v_name || '_unit_fk') THEN
|
|
343
|
+
EXECUTE format('ALTER TABLE %s ADD CONSTRAINT %I FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL', v_q, v_name || '_unit_fk');
|
|
344
|
+
END IF;
|
|
345
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = p_table AND conname = v_name || '_owner_fk') THEN
|
|
346
|
+
EXECUTE format('ALTER TABLE %s ADD CONSTRAINT %I FOREIGN KEY (tenant_id, owner_id) REFERENCES app.memberships(tenant_id, user_id) ON DELETE SET NULL', v_q, v_name || '_owner_fk');
|
|
347
|
+
END IF;
|
|
348
|
+
|
|
349
|
+
-- ── tenant-first indexes
|
|
350
|
+
EXECUTE format('CREATE INDEX IF NOT EXISTS %I ON %s (tenant_id, unit_id)', v_name || '_tenant_unit_idx', v_q);
|
|
351
|
+
EXECUTE format('CREATE INDEX IF NOT EXISTS %I ON %s (tenant_id, owner_id) WHERE owner_id IS NOT NULL', v_name || '_tenant_owner_idx', v_q);
|
|
352
|
+
|
|
353
|
+
-- ── updated_at
|
|
354
|
+
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %s', v_name || '_updated_at', v_q);
|
|
355
|
+
EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON %s FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()', v_name || '_updated_at', v_q);
|
|
356
|
+
|
|
357
|
+
-- ── RLS
|
|
358
|
+
EXECUTE format('ALTER TABLE %s ENABLE ROW LEVEL SECURITY', v_q);
|
|
359
|
+
EXECUTE format('ALTER TABLE %s FORCE ROW LEVEL SECURITY', v_q);
|
|
360
|
+
IF p_mode = 'enforce' THEN
|
|
361
|
+
FOR r IN SELECT policyname FROM pg_policies WHERE schemaname = v_schema AND tablename = v_name
|
|
362
|
+
AND policyname NOT IN (v_name || '_authz_select', v_name || '_authz_insert', v_name || '_authz_update', v_name || '_authz_delete') LOOP
|
|
363
|
+
v_prev := v_prev || r.policyname;
|
|
364
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON %s', r.policyname, v_q);
|
|
365
|
+
END LOOP;
|
|
366
|
+
END IF;
|
|
367
|
+
|
|
368
|
+
v_scope := app.scaffold_scope_sql(p_resource_type, p_plugin);
|
|
369
|
+
|
|
370
|
+
-- SELECT/UPDATE/DELETE varrem, então rendem pelo `scaffold_perm_sql`. O
|
|
371
|
+
-- WITH CHECK do INSERT não varre — ele julga a linha que está sendo escrita,
|
|
372
|
+
-- uma de cada vez — e por isso fica como estava.
|
|
373
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON %s', v_name || '_authz_select', v_q);
|
|
374
|
+
EXECUTE format('CREATE POLICY %I ON %s FOR SELECT TO authenticated USING (%s AND %s)',
|
|
375
|
+
v_name || '_authz_select', v_q, v_scope, app.scaffold_perm_sql(p_plugin || '.read'));
|
|
376
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON %s', v_name || '_authz_insert', v_q);
|
|
377
|
+
EXECUTE format('CREATE POLICY %I ON %s FOR INSERT TO authenticated WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (unit_id IS NULL OR (select app.has_unit(unit_id))) AND (select app.has_permission(%L, unit_id)))',
|
|
378
|
+
v_name || '_authz_insert', v_q, p_plugin || '.create');
|
|
379
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON %s', v_name || '_authz_update', v_q);
|
|
380
|
+
EXECUTE format('CREATE POLICY %I ON %s FOR UPDATE TO authenticated USING (%s AND %s) WITH CHECK (tenant_id = (select app.current_tenant_id()) AND (unit_id IS NULL OR (select app.has_unit(unit_id))) AND (select app.has_permission(%L, unit_id)))',
|
|
381
|
+
v_name || '_authz_update', v_q, v_scope, app.scaffold_perm_sql(p_plugin || '.edit'), p_plugin || '.edit');
|
|
382
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON %s', v_name || '_authz_delete', v_q);
|
|
383
|
+
EXECUTE format('CREATE POLICY %I ON %s FOR DELETE TO authenticated USING (%s AND %s)',
|
|
384
|
+
v_name || '_authz_delete', v_q, v_scope, app.scaffold_perm_sql(p_plugin || '.delete'));
|
|
385
|
+
|
|
386
|
+
-- scope defaults on REGISTRATION, so a table cannot exist in the registry
|
|
387
|
+
-- unclassified. A plugin's tables are scaffolded after the core chain has run,
|
|
388
|
+
-- so 136's DO block could never have reached them — they arrive here instead,
|
|
389
|
+
-- declared `unit_optional` until the plugin says otherwise.
|
|
390
|
+
INSERT INTO app.scaffold_registry (table_schema, table_name, resource_type, plugin, owner_scoping, mode, previous_policies, scope)
|
|
391
|
+
VALUES (v_schema, v_name, p_resource_type, p_plugin, p_owner_scoping, p_mode, v_prev, 'unit_optional')
|
|
392
|
+
ON CONFLICT (table_schema, table_name) DO UPDATE
|
|
393
|
+
SET resource_type = EXCLUDED.resource_type, plugin = EXCLUDED.plugin, owner_scoping = EXCLUDED.owner_scoping,
|
|
394
|
+
scope = coalesce(app.scaffold_registry.scope, EXCLUDED.scope),
|
|
395
|
+
mode = EXCLUDED.mode, applied_at = now(),
|
|
396
|
+
previous_policies = CASE WHEN EXCLUDED.mode = 'enforce' AND app.scaffold_registry.mode = 'shadow'
|
|
397
|
+
THEN app.scaffold_registry.previous_policies || EXCLUDED.previous_policies
|
|
398
|
+
ELSE EXCLUDED.previous_policies END;
|
|
399
|
+
END $$;
|
|
400
|
+
|
|
401
|
+
-- A view mascarada renderiza O MESMO predicado, com o mesmo `has_permission`
|
|
402
|
+
-- por linha. Herda a correção pela mesma porta.
|
|
403
|
+
CREATE OR REPLACE FUNCTION app.masked_view_refresh(p_table regclass, p_context text, p_reference text) RETURNS void
|
|
404
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
405
|
+
SET search_path TO ''
|
|
406
|
+
AS $$
|
|
407
|
+
DECLARE
|
|
408
|
+
v_schema text;
|
|
409
|
+
v_name text;
|
|
410
|
+
v_view text;
|
|
411
|
+
r app.scaffold_registry%ROWTYPE;
|
|
412
|
+
v_cols text;
|
|
413
|
+
v_class text;
|
|
414
|
+
v_structural constant text[] := ARRAY['id','tenant_id','unit_id','owner_id','created_at','updated_at'];
|
|
415
|
+
BEGIN
|
|
416
|
+
SELECT n.nspname, c.relname INTO v_schema, v_name
|
|
417
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.oid = p_table;
|
|
418
|
+
|
|
419
|
+
SELECT * INTO r FROM app.scaffold_registry WHERE table_schema = v_schema AND table_name = v_name;
|
|
420
|
+
IF NOT FOUND THEN
|
|
421
|
+
RAISE EXCEPTION 'masked_view_refresh: %.% is not scaffolded — the view renders the scaffold''s own predicate', v_schema, v_name
|
|
422
|
+
USING ERRCODE = 'P0002';
|
|
423
|
+
END IF;
|
|
424
|
+
IF NOT EXISTS (SELECT 1 FROM public.field_rule_contexts WHERE key = p_context) THEN
|
|
425
|
+
RAISE EXCEPTION 'masked_view_refresh: unknown field-rule context %', p_context USING ERRCODE = '23503';
|
|
426
|
+
END IF;
|
|
427
|
+
|
|
428
|
+
v_view := 'v_' || v_name;
|
|
429
|
+
|
|
430
|
+
SELECT string_agg(
|
|
431
|
+
CASE WHEN c.column_name = ANY (v_structural)
|
|
432
|
+
THEN format('%I', c.column_name)
|
|
433
|
+
-- `@>` and not `= ANY (…)`: a parenthesised scalar subquery is
|
|
434
|
+
-- parsed as ANY's SUBQUERY form, which compares against rows and
|
|
435
|
+
-- chokes on a text[]. Containment takes the array directly and
|
|
436
|
+
-- still plans as a once-per-statement InitPlan.
|
|
437
|
+
ELSE format('CASE WHEN NOT ((select app.hidden_fields(%L, %L)) @> ARRAY[%L::text]) THEN %I END AS %I',
|
|
438
|
+
p_context, p_reference, c.column_name, c.column_name, c.column_name)
|
|
439
|
+
END, E',\n ' ORDER BY c.ordinal_position)
|
|
440
|
+
INTO v_cols
|
|
441
|
+
FROM information_schema.columns c
|
|
442
|
+
WHERE c.table_schema = v_schema AND c.table_name = v_name;
|
|
443
|
+
|
|
444
|
+
SELECT CASE WHEN c.is_nullable = 'NO' THEN 'unit' ELSE NULL END INTO v_class
|
|
445
|
+
FROM information_schema.columns c
|
|
446
|
+
WHERE c.table_schema = v_schema AND c.table_name = v_name AND c.column_name = 'unit_id';
|
|
447
|
+
|
|
448
|
+
EXECUTE format('DROP VIEW IF EXISTS %I.%I', v_schema, v_view);
|
|
449
|
+
-- Owner rights on purpose (see the header): the predicate below IS the
|
|
450
|
+
-- scaffold's, rendered from the same function the policies render from.
|
|
451
|
+
EXECUTE format(
|
|
452
|
+
'CREATE VIEW %I.%I AS SELECT %s FROM %I.%I WHERE %s AND %s',
|
|
453
|
+
v_schema, v_view, v_cols, v_schema, v_name,
|
|
454
|
+
app.scaffold_scope_sql(r.resource_type, r.plugin, v_class),
|
|
455
|
+
app.scaffold_perm_sql(r.plugin || '.read', v_class));
|
|
456
|
+
|
|
457
|
+
EXECUTE format('REVOKE ALL ON %I.%I FROM public, anon', v_schema, v_view);
|
|
458
|
+
EXECUTE format('GRANT SELECT ON %I.%I TO authenticated, service_role', v_schema, v_view);
|
|
459
|
+
EXECUTE format(
|
|
460
|
+
'COMMENT ON VIEW %I.%I IS %L',
|
|
461
|
+
v_schema, v_view,
|
|
462
|
+
format('Masked read of %I.%I (#221): every non-structural column reads NULL when field_rules hides it for the caller''s role and unit. Owner rights with the scaffold''s own row predicate — regenerate with app.masked_view_refresh(''%I.%I'', %L, %L), never edit by hand.',
|
|
463
|
+
v_schema, v_name, v_schema, v_name, p_context, p_reference));
|
|
464
|
+
|
|
465
|
+
INSERT INTO app.masked_views (table_schema, table_name, view_name, context, reference)
|
|
466
|
+
VALUES (v_schema, v_name, v_view, p_context, p_reference)
|
|
467
|
+
ON CONFLICT (table_schema, table_name) DO UPDATE
|
|
468
|
+
SET view_name = EXCLUDED.view_name, context = EXCLUDED.context,
|
|
469
|
+
reference = EXCLUDED.reference, applied_at = now();
|
|
470
|
+
END $$;
|
|
471
|
+
|
|
472
|
+
-- ---------------------------------------------------------------------------
|
|
473
|
+
-- 6. Reescrever o que já está no banco. Um gerador corrigido não corrige nada
|
|
474
|
+
-- sozinho: o texto das políticas existentes foi assado quando elas foram
|
|
475
|
+
-- criadas. Idempotente — cada gerador derruba e recria a sua própria
|
|
476
|
+
-- política a partir do registro.
|
|
477
|
+
-- ---------------------------------------------------------------------------
|
|
478
|
+
DO $$
|
|
479
|
+
DECLARE r record; v_n int := 0;
|
|
480
|
+
BEGIN
|
|
481
|
+
FOR r IN SELECT table_schema, table_name FROM app.scaffold_registry ORDER BY 1, 2 LOOP
|
|
482
|
+
-- `public.items` e `public.item_associations` ficam de fora, e é a única
|
|
483
|
+
-- exceção nominal desta migration.
|
|
484
|
+
--
|
|
485
|
+
-- A permissão delas NÃO é constante por tabela: a linha de uma fatura pede
|
|
486
|
+
-- `financial.read`, a de uma remessa `inventory.read`, a de um pedido
|
|
487
|
+
-- `orders.read`, e quem decide é a `parent_kind` — coluna gerada, uma por
|
|
488
|
+
-- LINHA. O baseline escreveu essas quatro políticas à mão com esse CASE
|
|
489
|
+
-- dentro, e o renderizador genérico só sabe emitir um `plugin.ação` fixo:
|
|
490
|
+
-- regerá-las aqui achataria o despacho e tornaria uma linha de fatura
|
|
491
|
+
-- legível com `orders.read` sozinho. É fronteira de permissão, não estilo,
|
|
492
|
+
-- e a suíte 110_commercial_line existe para não deixar isso passar.
|
|
493
|
+
--
|
|
494
|
+
-- O custo de ficarem fora é a checagem por linha que a 016 tira das outras;
|
|
495
|
+
-- é um custo conhecido e escrito, e não foi delas que veio o teto de 8s.
|
|
496
|
+
-- Expressá-las no registro (uma coluna de despacho por plugin) é o
|
|
497
|
+
-- trabalho que fecha esta exceção.
|
|
498
|
+
CONTINUE WHEN r.table_schema = 'public' AND r.table_name IN ('items', 'item_associations');
|
|
499
|
+
-- Uma linha do registro pode apontar para uma tabela que já não existe
|
|
500
|
+
-- (plugin desinstalado). Pular é a resposta certa: não é papel desta
|
|
501
|
+
-- migration ter opinião sobre isso.
|
|
502
|
+
CONTINUE WHEN to_regclass(format('%I.%I', r.table_schema, r.table_name)) IS NULL;
|
|
503
|
+
-- E pode apontar para uma tabela que não tem as colunas do andaime: 16
|
|
504
|
+
-- linhas de plugin entraram no registro por INSERT direto, sem passar pelo
|
|
505
|
+
-- `scaffold_table` que as cria, e ficaram sem `unit_id`, sem `owner_id` ou
|
|
506
|
+
-- (uma) sem `id`. O gerador NUNCA conseguiu rodar nelas — o predicado que
|
|
507
|
+
-- ele escreve nomeia as quatro — e as políticas que elas têm foram escritas
|
|
508
|
+
-- à mão. Pular preserva o que já valia; tentar derrubaria a migration
|
|
509
|
+
-- inteira por causa de uma tabela que ela não governa.
|
|
510
|
+
--
|
|
511
|
+
-- É um desalinhamento real entre o registro e o esquema, e ele continua
|
|
512
|
+
-- depois desta migration. Fica o NOTICE; consertar é assunto do plugin.
|
|
513
|
+
CONTINUE WHEN EXISTS (
|
|
514
|
+
SELECT 1 FROM unnest(ARRAY['id', 'tenant_id', 'unit_id', 'owner_id']) col
|
|
515
|
+
WHERE NOT EXISTS (
|
|
516
|
+
SELECT 1 FROM information_schema.columns c
|
|
517
|
+
WHERE c.table_schema = r.table_schema AND c.table_name = r.table_name
|
|
518
|
+
AND c.column_name = col));
|
|
519
|
+
PERFORM app.scaffold_refresh_policies(r.table_schema || '.' || r.table_name);
|
|
520
|
+
v_n := v_n + 1;
|
|
521
|
+
END LOOP;
|
|
522
|
+
RAISE NOTICE '016: % tabela(s) com o predicado de varredura regerado', v_n;
|
|
523
|
+
|
|
524
|
+
v_n := 0;
|
|
525
|
+
FOR r IN SELECT table_schema, table_name, dist_resource_type, dist_key_column, dist_unit_escape
|
|
526
|
+
FROM app.scaffold_registry WHERE distributable ORDER BY 1, 2 LOOP
|
|
527
|
+
CONTINUE WHEN to_regclass(format('%I.%I', r.table_schema, r.table_name)) IS NULL;
|
|
528
|
+
PERFORM app.scaffold_distributable(
|
|
529
|
+
to_regclass(format('%I.%I', r.table_schema, r.table_name)),
|
|
530
|
+
r.dist_resource_type, coalesce(r.dist_key_column, 'id'), coalesce(r.dist_unit_escape, true), true);
|
|
531
|
+
v_n := v_n + 1;
|
|
532
|
+
END LOOP;
|
|
533
|
+
RAISE NOTICE '016: % política(s) de distribuição regerada(s)', v_n;
|
|
534
|
+
|
|
535
|
+
v_n := 0;
|
|
536
|
+
FOR r IN SELECT table_schema, table_name, context, reference FROM app.masked_views ORDER BY 1, 2 LOOP
|
|
537
|
+
CONTINUE WHEN to_regclass(format('%I.%I', r.table_schema, r.table_name)) IS NULL;
|
|
538
|
+
PERFORM app.masked_view_refresh(
|
|
539
|
+
to_regclass(format('%I.%I', r.table_schema, r.table_name)), r.context, r.reference);
|
|
540
|
+
v_n := v_n + 1;
|
|
541
|
+
END LOOP;
|
|
542
|
+
RAISE NOTICE '016: % view(s) mascarada(s) regerada(s)', v_n;
|
|
543
|
+
END $$;
|