@fayz-ai/db 0.22.0 → 0.22.3
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/141_o_endereco_da_unidade_mora_na_tabela_de_enderecos.sql +29 -5
- package/migrations/142_a_coordenada_e_do_endereco_nao_da_unidade.sql +28 -4
- package/migrations/172_o_pacote_pode_repetir_o_servico.sql +23 -0
- package/migrations/173_cada_unidade_alcancada_uma_vez.sql +25 -0
- package/migrations/174_a_conta_de_quem_recebe_e_dado_do_financeiro.sql +87 -0
- package/package.json +1 -1
|
@@ -69,6 +69,22 @@ SELECT u.tenant_id, 'unit', u.id, 'main',
|
|
|
69
69
|
AND (p.street <> '' OR p.city <> '');
|
|
70
70
|
|
|
71
71
|
-- ── 2. a view lê o endereço de onde ele mora ────────────────────────────────
|
|
72
|
+
-- Replay guard. 168 widened v_units with the company columns and rewrote its
|
|
73
|
+
-- writer. Re-running this file over that shape fails (CREATE OR REPLACE VIEW
|
|
74
|
+
-- cannot drop columns) and, if it did not, would put this file's older writer
|
|
75
|
+
-- back over 168's. So once the view carries `legal_entity_id`, the view and
|
|
76
|
+
-- the writer below are history and are skipped; what sits above this block is
|
|
77
|
+
-- idempotent and still runs.
|
|
78
|
+
DO $guard$
|
|
79
|
+
BEGIN
|
|
80
|
+
IF EXISTS (SELECT 1 FROM pg_attribute
|
|
81
|
+
WHERE attrelid = 'public.v_units'::regclass
|
|
82
|
+
AND attname = 'legal_entity_id' AND NOT attisdropped) THEN
|
|
83
|
+
RAISE NOTICE '141: v_units already has the 168 shape; view and writer left as they are';
|
|
84
|
+
RETURN;
|
|
85
|
+
END IF;
|
|
86
|
+
|
|
87
|
+
EXECUTE $s1$
|
|
72
88
|
CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
73
89
|
SELECT u.id,
|
|
74
90
|
u.tenant_id,
|
|
@@ -112,12 +128,16 @@ CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
|
112
128
|
WHERE x.owner_type = 'unit' AND x.owner_id = u.id
|
|
113
129
|
ORDER BY x.is_default DESC, x.created_at
|
|
114
130
|
LIMIT 1
|
|
115
|
-
) a ON true
|
|
131
|
+
) a ON true
|
|
132
|
+
$s1$;
|
|
116
133
|
|
|
134
|
+
EXECUTE $s2$
|
|
117
135
|
COMMENT ON VIEW public.v_units IS
|
|
118
|
-
'Units in the shape the units screen reads (#214), com o endereço vindo de public.addresses (141).'
|
|
136
|
+
'Units in the shape the units screen reads (#214), com o endereço vindo de public.addresses (141).'
|
|
137
|
+
$s2$;
|
|
119
138
|
|
|
120
|
-
-- ── 3. quem escreve o endereço é a tabela de endereços ──────────────────────
|
|
139
|
+
-- ── 3. quem escreve o endereço é a tabela de endereços ──────────────────────
|
|
140
|
+
EXECUTE $s3$
|
|
121
141
|
CREATE OR REPLACE FUNCTION app.trg_v_units_write() RETURNS trigger
|
|
122
142
|
LANGUAGE plpgsql
|
|
123
143
|
SET search_path TO ''
|
|
@@ -197,7 +217,11 @@ BEGIN
|
|
|
197
217
|
|
|
198
218
|
SELECT * INTO NEW FROM public.v_units WHERE id = v_id;
|
|
199
219
|
RETURN NEW;
|
|
200
|
-
END
|
|
220
|
+
END $$
|
|
221
|
+
$s3$;
|
|
201
222
|
|
|
223
|
+
EXECUTE $s4$
|
|
202
224
|
COMMENT ON FUNCTION app.trg_v_units_write() IS
|
|
203
|
-
'Escreve v_units em app.units; o endereço é de public.addresses e não passa por aqui (141).'
|
|
225
|
+
'Escreve v_units em app.units; o endereço é de public.addresses e não passa por aqui (141).'
|
|
226
|
+
$s4$;
|
|
227
|
+
END $guard$;
|
|
@@ -35,6 +35,22 @@ UPDATE app.units u
|
|
|
35
35
|
SET metadata = u.metadata - 'latitude' - 'longitude'
|
|
36
36
|
WHERE u.metadata ? 'latitude' OR u.metadata ? 'longitude';
|
|
37
37
|
|
|
38
|
+
-- Replay guard. 168 widened v_units with the company columns and rewrote its
|
|
39
|
+
-- writer. Re-running this file over that shape fails (CREATE OR REPLACE VIEW
|
|
40
|
+
-- cannot drop columns) and, if it did not, would put this file's older writer
|
|
41
|
+
-- back over 168's. So once the view carries `legal_entity_id`, the view and
|
|
42
|
+
-- the writer below are history and are skipped; what sits above this block is
|
|
43
|
+
-- idempotent and still runs.
|
|
44
|
+
DO $guard$
|
|
45
|
+
BEGIN
|
|
46
|
+
IF EXISTS (SELECT 1 FROM pg_attribute
|
|
47
|
+
WHERE attrelid = 'public.v_units'::regclass
|
|
48
|
+
AND attname = 'legal_entity_id' AND NOT attisdropped) THEN
|
|
49
|
+
RAISE NOTICE '142: v_units already has the 168 shape; view and writer left as they are';
|
|
50
|
+
RETURN;
|
|
51
|
+
END IF;
|
|
52
|
+
|
|
53
|
+
EXECUTE $s1$
|
|
38
54
|
CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
39
55
|
SELECT u.id,
|
|
40
56
|
u.tenant_id,
|
|
@@ -72,11 +88,15 @@ CREATE OR REPLACE VIEW public.v_units WITH (security_invoker='true') AS
|
|
|
72
88
|
WHERE x.owner_type = 'unit' AND x.owner_id = u.id
|
|
73
89
|
ORDER BY x.is_default DESC, x.created_at
|
|
74
90
|
LIMIT 1
|
|
75
|
-
) a ON true
|
|
91
|
+
) a ON true
|
|
92
|
+
$s1$;
|
|
76
93
|
|
|
94
|
+
EXECUTE $s2$
|
|
77
95
|
COMMENT ON VIEW public.v_units IS
|
|
78
|
-
'Units in the shape the units screen reads (#214); endereço e coordenada vêm de public.addresses (141, 142).'
|
|
96
|
+
'Units in the shape the units screen reads (#214); endereço e coordenada vêm de public.addresses (141, 142).'
|
|
97
|
+
$s2$;
|
|
79
98
|
|
|
99
|
+
EXECUTE $s3$
|
|
80
100
|
CREATE OR REPLACE FUNCTION app.trg_v_units_write() RETURNS trigger
|
|
81
101
|
LANGUAGE plpgsql
|
|
82
102
|
SET search_path TO ''
|
|
@@ -151,7 +171,11 @@ BEGIN
|
|
|
151
171
|
|
|
152
172
|
SELECT * INTO NEW FROM public.v_units WHERE id = v_id;
|
|
153
173
|
RETURN NEW;
|
|
154
|
-
END
|
|
174
|
+
END $$
|
|
175
|
+
$s3$;
|
|
155
176
|
|
|
177
|
+
EXECUTE $s4$
|
|
156
178
|
COMMENT ON FUNCTION app.trg_v_units_write() IS
|
|
157
|
-
'Escreve v_units em app.units; endereço e coordenada são de public.addresses (141, 142).'
|
|
179
|
+
'Escreve v_units em app.units; endereço e coordenada são de public.addresses (141, 142).'
|
|
180
|
+
$s4$;
|
|
181
|
+
END $guard$;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 172 — o pacote pode repetir o mesmo serviço em linhas diferentes.
|
|
3
|
+
--
|
|
4
|
+
-- `service_package_items_unique (package_id, product_id)` veio do porte e
|
|
5
|
+
-- barrava o que a grade do pacote precisa fazer: duplicar uma linha (o mesmo
|
|
6
|
+
-- serviço com outro valor no pacote — a primeira sessão cheia, as demais com
|
|
7
|
+
-- desconto) e trocar o serviço de uma linha para um que já está em outra.
|
|
8
|
+
-- O erro chegava cru na tela: «duplicate key value violates unique constraint».
|
|
9
|
+
--
|
|
10
|
+
-- Nada lia a unicidade: nenhum ON CONFLICT, e quem consome o pacote (venda,
|
|
11
|
+
-- saldo de sessões) itera as linhas. A soma da 166 já é por linha.
|
|
12
|
+
--
|
|
13
|
+
-- O índice da constraint era o único que começava por `package_id`; a lista
|
|
14
|
+
-- do pacote filtra por ele, então um índice simples entra no lugar.
|
|
15
|
+
--
|
|
16
|
+
-- Idempotente e sem tocar em dado.
|
|
17
|
+
-- ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
ALTER TABLE public.service_package_items
|
|
20
|
+
DROP CONSTRAINT IF EXISTS service_package_items_unique;
|
|
21
|
+
|
|
22
|
+
CREATE INDEX IF NOT EXISTS service_package_items_package_idx
|
|
23
|
+
ON public.service_package_items (tenant_id, package_id);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- The reach walk UNIONs (id, depth): a branch the person reaches directly AND
|
|
2
|
+
-- through its HQ came back twice, and every unit picker listed it twice.
|
|
3
|
+
-- Same body as 017, deduplicated on the way out.
|
|
4
|
+
|
|
5
|
+
CREATE OR REPLACE FUNCTION app.user_unit_ids_in(p_tenant uuid)
|
|
6
|
+
RETURNS SETOF uuid
|
|
7
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
8
|
+
SET search_path TO ''
|
|
9
|
+
AS $$
|
|
10
|
+
WITH RECURSIVE seed AS (
|
|
11
|
+
SELECT a.unit_id AS id, 0 AS depth
|
|
12
|
+
FROM app.user_unit_access a
|
|
13
|
+
WHERE a.user_id = (SELECT auth.uid())
|
|
14
|
+
AND a.tenant_id = p_tenant
|
|
15
|
+
),
|
|
16
|
+
tree AS (
|
|
17
|
+
SELECT id, depth FROM seed
|
|
18
|
+
UNION
|
|
19
|
+
SELECT u.id, t.depth + 1
|
|
20
|
+
FROM app.units u
|
|
21
|
+
JOIN tree t ON u.parent_unit_id = t.id
|
|
22
|
+
WHERE u.tenant_id = p_tenant AND t.depth < 8
|
|
23
|
+
)
|
|
24
|
+
SELECT DISTINCT id FROM tree;
|
|
25
|
+
$$;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- 174 — a conta de quem recebe é dado do financeiro.
|
|
3
|
+
--
|
|
4
|
+
-- Three permissive policies that let any member of a tenant do what the gated
|
|
5
|
+
-- policies next to them refuse. Postgres ORs permissive policies, so a
|
|
6
|
+
-- tenant-only policy beside an `app.has_permission` one wins every time.
|
|
7
|
+
--
|
|
8
|
+
-- · payee_accounts (149): bank/PIX of whoever the house pays. Any member,
|
|
9
|
+
-- viewer and SDR included, could rewrite where a payout lands. Now read
|
|
10
|
+
-- needs financial.read; insert/update financial.edit; delete
|
|
11
|
+
-- financial.delete (financial.manage satisfies all three).
|
|
12
|
+
-- · fulfillments: `fulfillments_member_all` (FOR ALL) shadowed the
|
|
13
|
+
-- fulfillments_authz_* write policies. The write side goes; a tenant-wide
|
|
14
|
+
-- SELECT stays so shop screens and plg_shop_rep_orders keep reading as today.
|
|
15
|
+
-- Every writer outside the authz policies is SECURITY DEFINER or the
|
|
16
|
+
-- service role.
|
|
17
|
+
-- · audit_logs: any member could insert any action, backdate it, stamp
|
|
18
|
+
-- before/after and pick a unit. The CRUD trail (admin stores/audit.ts) is
|
|
19
|
+
-- the only client writer and only writes `record.*`; server namespaces are
|
|
20
|
+
-- written by SECURITY DEFINER functions owned by postgres, which bypass RLS.
|
|
21
|
+
-- audit_log() leaves the client surface for the same reason.
|
|
22
|
+
--
|
|
23
|
+
-- Additive and replay-safe: DROP POLICY IF EXISTS + CREATE POLICY.
|
|
24
|
+
-- ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
-- ── payee_accounts ──────────────────────────────────────────────────────
|
|
27
|
+
DROP POLICY IF EXISTS payee_accounts_member_all ON public.payee_accounts;
|
|
28
|
+
|
|
29
|
+
DROP POLICY IF EXISTS payee_accounts_authz_select ON public.payee_accounts;
|
|
30
|
+
CREATE POLICY payee_accounts_authz_select ON public.payee_accounts
|
|
31
|
+
FOR SELECT TO authenticated
|
|
32
|
+
USING (tenant_id = (SELECT app.current_tenant_id())
|
|
33
|
+
AND (SELECT app.has_permission('financial.read')));
|
|
34
|
+
|
|
35
|
+
DROP POLICY IF EXISTS payee_accounts_authz_insert ON public.payee_accounts;
|
|
36
|
+
CREATE POLICY payee_accounts_authz_insert ON public.payee_accounts
|
|
37
|
+
FOR INSERT TO authenticated
|
|
38
|
+
WITH CHECK (tenant_id = (SELECT app.current_tenant_id())
|
|
39
|
+
AND (SELECT app.has_permission('financial.edit')));
|
|
40
|
+
|
|
41
|
+
-- trg_payee_accounts_one_default is SECURITY INVOKER: its demote-the-old-default
|
|
42
|
+
-- UPDATE runs under this policy too, which is why insert also asks for edit.
|
|
43
|
+
DROP POLICY IF EXISTS payee_accounts_authz_update ON public.payee_accounts;
|
|
44
|
+
CREATE POLICY payee_accounts_authz_update ON public.payee_accounts
|
|
45
|
+
FOR UPDATE TO authenticated
|
|
46
|
+
USING (tenant_id = (SELECT app.current_tenant_id())
|
|
47
|
+
AND (SELECT app.has_permission('financial.edit')))
|
|
48
|
+
WITH CHECK (tenant_id = (SELECT app.current_tenant_id())
|
|
49
|
+
AND (SELECT app.has_permission('financial.edit')));
|
|
50
|
+
|
|
51
|
+
DROP POLICY IF EXISTS payee_accounts_authz_delete ON public.payee_accounts;
|
|
52
|
+
CREATE POLICY payee_accounts_authz_delete ON public.payee_accounts
|
|
53
|
+
FOR DELETE TO authenticated
|
|
54
|
+
USING (tenant_id = (SELECT app.current_tenant_id())
|
|
55
|
+
AND (SELECT app.has_permission('financial.delete')));
|
|
56
|
+
|
|
57
|
+
-- ── fulfillments ────────────────────────────────────────────────────────
|
|
58
|
+
DROP POLICY IF EXISTS fulfillments_member_all ON public.fulfillments;
|
|
59
|
+
|
|
60
|
+
DROP POLICY IF EXISTS fulfillments_member_read ON public.fulfillments;
|
|
61
|
+
CREATE POLICY fulfillments_member_read ON public.fulfillments
|
|
62
|
+
FOR SELECT TO authenticated
|
|
63
|
+
USING (tenant_id = (SELECT app.current_tenant_id()));
|
|
64
|
+
|
|
65
|
+
-- ── audit_logs ──────────────────────────────────────────────────────────
|
|
66
|
+
-- Checked after the BEFORE trigger (handle_audit_actor), so user_id and
|
|
67
|
+
-- tenant_id are already the session's here.
|
|
68
|
+
DROP POLICY IF EXISTS audit_insert ON public.audit_logs;
|
|
69
|
+
CREATE POLICY audit_insert ON public.audit_logs
|
|
70
|
+
FOR INSERT TO authenticated
|
|
71
|
+
WITH CHECK (
|
|
72
|
+
tenant_id = (SELECT app.current_tenant_id())
|
|
73
|
+
AND user_id = (SELECT auth.uid())
|
|
74
|
+
AND action LIKE 'record.%'
|
|
75
|
+
AND before IS NULL
|
|
76
|
+
AND after IS NULL
|
|
77
|
+
AND (unit_id IS NULL OR (SELECT app.has_unit(unit_id)))
|
|
78
|
+
AND created_at BETWEEN now() - interval '1 minute' AND now() + interval '1 minute'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
-- audit_log() is SECURITY DEFINER and takes any action, before/after and unit:
|
|
82
|
+
-- open to members it is the same forgery by another door. Nothing calls it
|
|
83
|
+
-- from a client or another function; server code keeps it via service_role.
|
|
84
|
+
REVOKE EXECUTE ON FUNCTION public.audit_log(text, text, uuid, text, jsonb, jsonb, uuid, jsonb, uuid)
|
|
85
|
+
FROM PUBLIC, anon, authenticated;
|
|
86
|
+
GRANT EXECUTE ON FUNCTION public.audit_log(text, text, uuid, text, jsonb, jsonb, uuid, jsonb, uuid)
|
|
87
|
+
TO service_role;
|
package/package.json
CHANGED