@fayz-ai/db 0.24.1 → 0.24.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/209_the_store_gets_its_rail_group.sql +37 -0
- package/migrations/210_the_service_speaks_as_the_caller.sql +32 -0
- package/migrations/211_the_service_door_for_places_and_connections.sql +77 -0
- package/migrations/212_the_service_acts_for_the_store.sql +65 -0
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 209 — the store gets a "Loja" group in the ChefControl and GoControl rails
|
|
3
|
+
--
|
|
4
|
+
-- A tenant's rail is its FIRST app's preset (app_config orders trades first).
|
|
5
|
+
-- Artorius runs ChefControl + StoreControl + GoControl, so it draws the
|
|
6
|
+
-- ChefControl rail, which has no group for the shop's screens: Descontos and
|
|
7
|
+
-- Conteúdo fell loose at the bottom, and nothing said "Loja". StoreControl's
|
|
8
|
+
-- own rail has the group; the trades that also sell online did not.
|
|
9
|
+
--
|
|
10
|
+
-- The group only collects routes that exist: a tenant without the shop sees
|
|
11
|
+
-- no empty heading. /products and /orders stay where these rails already put
|
|
12
|
+
-- them (Estoque, Operação).
|
|
13
|
+
--
|
|
14
|
+
-- Placed right after the Estoque group. Idempotent: skipped where a "Loja"
|
|
15
|
+
-- group is already present.
|
|
16
|
+
-- ============================================================================
|
|
17
|
+
|
|
18
|
+
UPDATE app.apps a
|
|
19
|
+
SET preset = jsonb_set(
|
|
20
|
+
a.preset,
|
|
21
|
+
'{rail}',
|
|
22
|
+
jsonb_insert(
|
|
23
|
+
a.preset -> 'rail',
|
|
24
|
+
ARRAY[(
|
|
25
|
+
SELECT (e.ord)::text
|
|
26
|
+
FROM jsonb_array_elements(a.preset -> 'rail') WITH ORDINALITY AS e(item, ord)
|
|
27
|
+
WHERE e.item ->> 'label' = 'Estoque'
|
|
28
|
+
LIMIT 1
|
|
29
|
+
)],
|
|
30
|
+
'{"icon":"ShoppingBag","label":"Loja","under":["/discounts","/content","/shop"]}'::jsonb
|
|
31
|
+
)
|
|
32
|
+
),
|
|
33
|
+
updated_at = now()
|
|
34
|
+
WHERE a.id IN ('resto', 'full')
|
|
35
|
+
AND jsonb_typeof(a.preset -> 'rail') = 'array'
|
|
36
|
+
AND EXISTS (SELECT 1 FROM jsonb_array_elements(a.preset -> 'rail') e WHERE e ->> 'label' = 'Estoque')
|
|
37
|
+
AND NOT EXISTS (SELECT 1 FROM jsonb_array_elements(a.preset -> 'rail') e WHERE e ->> 'label' = 'Loja');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 210_the_service_speaks_as_the_caller — the integration service (ADR 0003,
|
|
3
|
+
-- phase 2) reaches the cluster the way PostgREST does, never with a
|
|
4
|
+
-- service-role key.
|
|
5
|
+
--
|
|
6
|
+
-- PostgREST connects as `authenticator`, a NOINHERIT role that is a member of
|
|
7
|
+
-- anon and authenticated: it holds no privilege of its own and, per request,
|
|
8
|
+
-- `SET LOCAL ROLE`s into the caller's role with the caller's JWT claims, so
|
|
9
|
+
-- RLS and app.has_permission decide exactly as they do for the browser.
|
|
10
|
+
--
|
|
11
|
+
-- fayz_integration becomes the same kind of role. It stays NOINHERIT, so
|
|
12
|
+
-- membership grants nothing until the service switches role inside a
|
|
13
|
+
-- transaction for a request whose JWT it has verified. Machine work (webhooks,
|
|
14
|
+
-- jobs) keeps going through named SECURITY DEFINER functions granted to
|
|
15
|
+
-- fayz_integration directly; service_role is never granted.
|
|
16
|
+
--
|
|
17
|
+
-- Guarded: the role is an operator step and may not exist on a fresh cluster.
|
|
18
|
+
-- Replay-safe: GRANT of an existing membership is a no-op.
|
|
19
|
+
-- ============================================================================
|
|
20
|
+
|
|
21
|
+
DO $$
|
|
22
|
+
BEGIN
|
|
23
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN
|
|
24
|
+
ALTER ROLE fayz_integration NOINHERIT;
|
|
25
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
|
|
26
|
+
GRANT authenticated TO fayz_integration;
|
|
27
|
+
END IF;
|
|
28
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
|
|
29
|
+
GRANT anon TO fayz_integration;
|
|
30
|
+
END IF;
|
|
31
|
+
END IF;
|
|
32
|
+
END $$;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 211_the_service_door_for_places_and_connections — named functions the
|
|
3
|
+
-- integration service (ADR 0003 phase 2) calls for machine work that touches
|
|
4
|
+
-- core tables: the postcode cache and a tenant's connector row.
|
|
5
|
+
--
|
|
6
|
+
-- postal_area_get(code) the cached address for a CEP, or null
|
|
7
|
+
-- postal_area_remember(...) caches a looked-up CEP (keys via area_key)
|
|
8
|
+
-- connection_get(tenant, connector, instance)
|
|
9
|
+
-- {id, active, status, settings} or null
|
|
10
|
+
-- connection_settings_merge(id, patch)
|
|
11
|
+
-- merges keys into settings (the provider's
|
|
12
|
+
-- own bookkeeping, e.g. a webhook endpoint id)
|
|
13
|
+
--
|
|
14
|
+
-- SECURITY DEFINER, closed to anon/authenticated, granted to service_role and,
|
|
15
|
+
-- when it exists, fayz_integration. Replay-safe: CREATE OR REPLACE.
|
|
16
|
+
-- ============================================================================
|
|
17
|
+
|
|
18
|
+
CREATE OR REPLACE FUNCTION public.postal_area_get(p_code text) RETURNS jsonb
|
|
19
|
+
LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO ''
|
|
20
|
+
AS $function$
|
|
21
|
+
SELECT jsonb_build_object(
|
|
22
|
+
'postal_code', a.postal_code, 'street', a.street, 'district', a.district,
|
|
23
|
+
'city', a.city, 'city_code', a.city_code, 'state', a.state)
|
|
24
|
+
FROM public.postal_areas a
|
|
25
|
+
WHERE a.postal_code = regexp_replace(coalesce(p_code, ''), '\D', '', 'g');
|
|
26
|
+
$function$;
|
|
27
|
+
|
|
28
|
+
CREATE OR REPLACE FUNCTION public.postal_area_remember(
|
|
29
|
+
p_code text, p_street text, p_district text, p_city text, p_city_code text, p_state text, p_source text
|
|
30
|
+
) RETURNS void
|
|
31
|
+
LANGUAGE sql SECURITY DEFINER SET search_path TO ''
|
|
32
|
+
AS $function$
|
|
33
|
+
INSERT INTO public.postal_areas (postal_code, street, district, district_key, city, city_key, city_code, state, source, fetched_at)
|
|
34
|
+
VALUES (
|
|
35
|
+
regexp_replace(p_code, '\D', '', 'g'), nullif(p_street, ''), nullif(p_district, ''),
|
|
36
|
+
public.area_key(nullif(p_district, '')), p_city, public.area_key(p_city),
|
|
37
|
+
nullif(p_city_code, ''), nullif(upper(p_state), ''), coalesce(p_source, 'viacep'), now())
|
|
38
|
+
ON CONFLICT (postal_code) DO UPDATE
|
|
39
|
+
SET street = excluded.street, district = excluded.district, district_key = excluded.district_key,
|
|
40
|
+
city = excluded.city, city_key = excluded.city_key, city_code = excluded.city_code,
|
|
41
|
+
state = excluded.state, source = excluded.source, fetched_at = excluded.fetched_at;
|
|
42
|
+
$function$;
|
|
43
|
+
|
|
44
|
+
CREATE OR REPLACE FUNCTION public.connection_get(p_tenant uuid, p_connector text, p_instance text DEFAULT '')
|
|
45
|
+
RETURNS jsonb
|
|
46
|
+
LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO ''
|
|
47
|
+
AS $function$
|
|
48
|
+
SELECT jsonb_build_object('id', c.id, 'active', c.active, 'status', c.status, 'settings', coalesce(c.settings, '{}'::jsonb))
|
|
49
|
+
FROM public.connections c
|
|
50
|
+
WHERE c.tenant_id = p_tenant AND c.connector_id = p_connector AND c.instance_key = coalesce(p_instance, '');
|
|
51
|
+
$function$;
|
|
52
|
+
|
|
53
|
+
CREATE OR REPLACE FUNCTION public.connection_settings_merge(p_connection uuid, p_patch jsonb) RETURNS void
|
|
54
|
+
LANGUAGE sql SECURITY DEFINER SET search_path TO ''
|
|
55
|
+
AS $function$
|
|
56
|
+
UPDATE public.connections
|
|
57
|
+
SET settings = coalesce(settings, '{}'::jsonb) || coalesce(p_patch, '{}'::jsonb), updated_at = now()
|
|
58
|
+
WHERE id = p_connection;
|
|
59
|
+
$function$;
|
|
60
|
+
|
|
61
|
+
DO $$
|
|
62
|
+
DECLARE
|
|
63
|
+
f text;
|
|
64
|
+
BEGIN
|
|
65
|
+
FOREACH f IN ARRAY ARRAY[
|
|
66
|
+
'public.postal_area_get(text)',
|
|
67
|
+
'public.postal_area_remember(text, text, text, text, text, text, text)',
|
|
68
|
+
'public.connection_get(uuid, text, text)',
|
|
69
|
+
'public.connection_settings_merge(uuid, jsonb)'
|
|
70
|
+
] LOOP
|
|
71
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC', f);
|
|
72
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN EXECUTE format('REVOKE ALL ON FUNCTION %s FROM anon', f); END IF;
|
|
73
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN EXECUTE format('REVOKE ALL ON FUNCTION %s FROM authenticated', f); END IF;
|
|
74
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', f); END IF;
|
|
75
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO fayz_integration', f); END IF;
|
|
76
|
+
END LOOP;
|
|
77
|
+
END $$;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 212_the_service_acts_for_the_store — machine work (a webhook, a scheduled
|
|
3
|
+
-- sync) is done by the integration service AS THE STORE'S OWNER, under RLS,
|
|
4
|
+
-- the same rule shop migration 0119 uses for a server-confirmed payment.
|
|
5
|
+
--
|
|
6
|
+
-- service_actor_for(tenant) answers the earliest active, all-units owner of
|
|
7
|
+
-- the tenant. The service then runs the work in one transaction under
|
|
8
|
+
-- SET LOCAL ROLE authenticated with that user's claims (migration 210), so
|
|
9
|
+
-- policies, permissions and audit columns see a real member, and nothing
|
|
10
|
+
-- bypasses RLS. A tenant with no active owner answers NULL and the service
|
|
11
|
+
-- refuses the work rather than doing it as nobody.
|
|
12
|
+
--
|
|
13
|
+
-- Granted to fayz_integration only (and service_role). Replay-safe.
|
|
14
|
+
-- ============================================================================
|
|
15
|
+
|
|
16
|
+
CREATE OR REPLACE FUNCTION public.service_actor_for(p_tenant uuid) RETURNS uuid
|
|
17
|
+
LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO ''
|
|
18
|
+
AS $function$
|
|
19
|
+
SELECT m.user_id
|
|
20
|
+
FROM app.memberships m
|
|
21
|
+
JOIN app.roles r ON r.id = m.role_id
|
|
22
|
+
WHERE m.tenant_id = p_tenant AND m.active AND m.all_units AND r.key = 'owner'
|
|
23
|
+
ORDER BY m.created_at, m.user_id
|
|
24
|
+
LIMIT 1;
|
|
25
|
+
$function$;
|
|
26
|
+
|
|
27
|
+
DO $$
|
|
28
|
+
BEGIN
|
|
29
|
+
REVOKE ALL ON FUNCTION public.service_actor_for(uuid) FROM PUBLIC;
|
|
30
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN REVOKE ALL ON FUNCTION public.service_actor_for(uuid) FROM anon; END IF;
|
|
31
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN REVOKE ALL ON FUNCTION public.service_actor_for(uuid) FROM authenticated; END IF;
|
|
32
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN GRANT EXECUTE ON FUNCTION public.service_actor_for(uuid) TO service_role; END IF;
|
|
33
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') THEN GRANT EXECUTE ON FUNCTION public.service_actor_for(uuid) TO fayz_integration; END IF;
|
|
34
|
+
END $$;
|
|
35
|
+
|
|
36
|
+
-- ── sync_runs: written by machines only ─────────────────────────────────────
|
|
37
|
+
-- A connector's sync log. Panels read it (SELECT policy for authenticated);
|
|
38
|
+
-- only a sync writes it, and the sync now runs in the service. The service
|
|
39
|
+
-- role gets exactly what service_role had here, scoped to this table.
|
|
40
|
+
DO $$
|
|
41
|
+
BEGIN
|
|
42
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fayz_integration') AND to_regclass('public.sync_runs') IS NOT NULL THEN
|
|
43
|
+
GRANT SELECT, INSERT, UPDATE ON public.sync_runs TO fayz_integration;
|
|
44
|
+
DROP POLICY IF EXISTS sync_runs_service ON public.sync_runs;
|
|
45
|
+
CREATE POLICY sync_runs_service ON public.sync_runs FOR ALL TO fayz_integration USING (true) WITH CHECK (true);
|
|
46
|
+
END IF;
|
|
47
|
+
END $$;
|
|
48
|
+
|
|
49
|
+
-- ── service_caller_belongs: "is this tenant yours?", asked as the caller ─────
|
|
50
|
+
-- The service verifies a caller's JWT, then asks the cluster, in the caller's
|
|
51
|
+
-- own role, whether the tenant they named is one of theirs. The answer comes
|
|
52
|
+
-- from user_tenant_ids(), the same set RLS uses. SECURITY INVOKER on purpose:
|
|
53
|
+
-- it says nothing a member could not already learn about themselves.
|
|
54
|
+
CREATE OR REPLACE FUNCTION public.service_caller_belongs(p_tenant uuid) RETURNS boolean
|
|
55
|
+
LANGUAGE sql STABLE SET search_path TO ''
|
|
56
|
+
AS $function$
|
|
57
|
+
SELECT p_tenant IN (SELECT public.user_tenant_ids());
|
|
58
|
+
$function$;
|
|
59
|
+
|
|
60
|
+
DO $$
|
|
61
|
+
BEGIN
|
|
62
|
+
REVOKE ALL ON FUNCTION public.service_caller_belongs(uuid) FROM PUBLIC;
|
|
63
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN REVOKE ALL ON FUNCTION public.service_caller_belongs(uuid) FROM anon; END IF;
|
|
64
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN GRANT EXECUTE ON FUNCTION public.service_caller_belongs(uuid) TO authenticated; END IF;
|
|
65
|
+
END $$;
|
package/package.json
CHANGED