@fayz-ai/db 0.24.3 → 0.25.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.
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 213_the_service_reads_facts_and_rings_the_bell — two doors for the
|
|
3
|
+
-- Integration service, so a fact recorded here can reach a person through it.
|
|
4
|
+
--
|
|
5
|
+
-- ADR 0003: the database records what happened (event_log) and answers who may
|
|
6
|
+
-- see what; the service decides who is TOLD, and how. The first user is the
|
|
7
|
+
-- low-stock alert (plugin-inventory 031 records `inventory.stock_low`; the
|
|
8
|
+
-- service turns it into a bell line for whoever can read the stock, and is
|
|
9
|
+
-- where WhatsApp and e-mail join later).
|
|
10
|
+
--
|
|
11
|
+
-- event_log_for_service(keys, since, limit)
|
|
12
|
+
-- The facts of the given keys recorded since a moment, oldest first. The
|
|
13
|
+
-- service keeps its own receipt of what it handled and asks for a window
|
|
14
|
+
-- rather than a cursor: a transaction that committed late can carry a
|
|
15
|
+
-- created_at older than the newest fact already read, and a cursor would
|
|
16
|
+
-- step over it.
|
|
17
|
+
--
|
|
18
|
+
-- notifications_ring_for_service(tenant, permission, unit, type, title, body,
|
|
19
|
+
-- link, subject_type, subject_id, at)
|
|
20
|
+
-- One bell line per active member whose effective role holds `permission`
|
|
21
|
+
-- (or the `.manage` of its family) for `unit` — the same rule as
|
|
22
|
+
-- app._has_permission_raw, asked here for every member instead of re-made in
|
|
23
|
+
-- the service. The unique index of 130 on (user, subject_type, subject_id,
|
|
24
|
+
-- type) keeps one line per person per subject: a newer ring marks it unread
|
|
25
|
+
-- and moves it up; replaying the same ring (same `at`) changes nothing. The
|
|
26
|
+
-- bell is poked over realtime as the order bell does. Returns how many people
|
|
27
|
+
-- the line reached.
|
|
28
|
+
--
|
|
29
|
+
-- Granted to the service's role only (and service_role). Replay-safe.
|
|
30
|
+
-- ============================================================================
|
|
31
|
+
|
|
32
|
+
CREATE OR REPLACE FUNCTION public.event_log_for_service(
|
|
33
|
+
p_keys text[],
|
|
34
|
+
p_since timestamptz,
|
|
35
|
+
p_limit integer DEFAULT 200
|
|
36
|
+
)
|
|
37
|
+
RETURNS TABLE (
|
|
38
|
+
id uuid,
|
|
39
|
+
tenant_id uuid,
|
|
40
|
+
event_key text,
|
|
41
|
+
payload jsonb,
|
|
42
|
+
subject_type text,
|
|
43
|
+
subject_id text,
|
|
44
|
+
occurred_at timestamptz,
|
|
45
|
+
created_at timestamptz
|
|
46
|
+
)
|
|
47
|
+
LANGUAGE sql
|
|
48
|
+
STABLE
|
|
49
|
+
SECURITY DEFINER
|
|
50
|
+
SET search_path TO ''
|
|
51
|
+
AS $function$
|
|
52
|
+
SELECT e.id, e.tenant_id, e.event_key, e.payload, e.subject_type, e.subject_id,
|
|
53
|
+
e.occurred_at, e.created_at
|
|
54
|
+
FROM public.event_log e
|
|
55
|
+
WHERE e.event_key = ANY (p_keys)
|
|
56
|
+
AND e.created_at >= p_since
|
|
57
|
+
ORDER BY e.created_at, e.id
|
|
58
|
+
LIMIT least(greatest(coalesce(p_limit, 200), 1), 1000);
|
|
59
|
+
$function$;
|
|
60
|
+
|
|
61
|
+
CREATE OR REPLACE FUNCTION public.notifications_ring_for_service(
|
|
62
|
+
p_tenant uuid,
|
|
63
|
+
p_permission text,
|
|
64
|
+
p_unit uuid,
|
|
65
|
+
p_type text,
|
|
66
|
+
p_title text,
|
|
67
|
+
p_body text,
|
|
68
|
+
p_link text,
|
|
69
|
+
p_subject_type text,
|
|
70
|
+
p_subject_id uuid,
|
|
71
|
+
p_at timestamptz DEFAULT now()
|
|
72
|
+
)
|
|
73
|
+
RETURNS integer
|
|
74
|
+
LANGUAGE plpgsql
|
|
75
|
+
SECURITY DEFINER
|
|
76
|
+
SET search_path TO ''
|
|
77
|
+
AS $function$
|
|
78
|
+
DECLARE
|
|
79
|
+
v_reached integer;
|
|
80
|
+
BEGIN
|
|
81
|
+
IF p_tenant IS NULL OR coalesce(p_permission, '') = '' OR coalesce(p_type, '') = ''
|
|
82
|
+
OR coalesce(p_title, '') = '' OR p_subject_id IS NULL THEN
|
|
83
|
+
RAISE EXCEPTION 'notifications_ring_for_service: tenant, permission, type, title and subject are required'
|
|
84
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
85
|
+
END IF;
|
|
86
|
+
|
|
87
|
+
WITH recipients AS (
|
|
88
|
+
SELECT DISTINCT m.user_id
|
|
89
|
+
FROM app.memberships m
|
|
90
|
+
WHERE m.tenant_id = p_tenant AND m.active AND m.user_id IS NOT NULL
|
|
91
|
+
AND EXISTS (
|
|
92
|
+
SELECT 1 FROM app.role_permissions rp
|
|
93
|
+
WHERE rp.tenant_id = p_tenant
|
|
94
|
+
AND rp.role_id = app.effective_role_id(p_tenant, m.user_id, p_unit)
|
|
95
|
+
AND rp.permission IN (p_permission, regexp_replace(p_permission, '\.[^.]+$', '.manage')))
|
|
96
|
+
), rung AS (
|
|
97
|
+
INSERT INTO public.notifications (tenant_id, user_id, title, body, type, link, subject_type, subject_id, created_at)
|
|
98
|
+
SELECT p_tenant, r.user_id, p_title, p_body, p_type, p_link, p_subject_type, p_subject_id, coalesce(p_at, now())
|
|
99
|
+
FROM recipients r
|
|
100
|
+
ON CONFLICT (user_id, subject_type, subject_id, type) WHERE subject_id IS NOT NULL
|
|
101
|
+
DO UPDATE SET read = false, created_at = EXCLUDED.created_at,
|
|
102
|
+
title = EXCLUDED.title, body = EXCLUDED.body, link = EXCLUDED.link
|
|
103
|
+
WHERE public.notifications.created_at < EXCLUDED.created_at
|
|
104
|
+
RETURNING 1
|
|
105
|
+
)
|
|
106
|
+
SELECT count(*)::integer INTO v_reached FROM rung;
|
|
107
|
+
|
|
108
|
+
IF v_reached > 0 AND to_regproc('realtime.send') IS NOT NULL THEN
|
|
109
|
+
BEGIN
|
|
110
|
+
PERFORM realtime.send(
|
|
111
|
+
jsonb_build_object('type', p_type, 'subjectId', p_subject_id),
|
|
112
|
+
'notification',
|
|
113
|
+
-- Contract with `subscribeBell()` in packages/admin/src/lib/bell.ts.
|
|
114
|
+
'avisos:' || p_tenant::text,
|
|
115
|
+
false);
|
|
116
|
+
EXCEPTION WHEN OTHERS THEN
|
|
117
|
+
RAISE WARNING 'notifications_ring_for_service: realtime: %', SQLERRM;
|
|
118
|
+
END;
|
|
119
|
+
END IF;
|
|
120
|
+
|
|
121
|
+
RETURN v_reached;
|
|
122
|
+
END;
|
|
123
|
+
$function$;
|
|
124
|
+
|
|
125
|
+
DO $$
|
|
126
|
+
DECLARE
|
|
127
|
+
f text;
|
|
128
|
+
BEGIN
|
|
129
|
+
FOREACH f IN ARRAY ARRAY[
|
|
130
|
+
'public.event_log_for_service(text[], timestamptz, integer)',
|
|
131
|
+
'public.notifications_ring_for_service(uuid, text, uuid, text, text, text, text, text, uuid, timestamptz)'
|
|
132
|
+
] LOOP
|
|
133
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC', f);
|
|
134
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN EXECUTE format('REVOKE ALL ON FUNCTION %s FROM anon', f); END IF;
|
|
135
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN EXECUTE format('REVOKE ALL ON FUNCTION %s FROM authenticated', f); END IF;
|
|
136
|
+
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;
|
|
137
|
+
-- The service's role, under its current name and the name it is moving to
|
|
138
|
+
-- (control_integration): granted to whichever exists, so this file is
|
|
139
|
+
-- right before and after the rename.
|
|
140
|
+
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;
|
|
141
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'control_integration') THEN EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO control_integration', f); END IF;
|
|
142
|
+
END LOOP;
|
|
143
|
+
END $$;
|
package/package.json
CHANGED