dzql 0.5.33 → 0.6.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/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +293 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +641 -0
- package/docs/project-setup.md +432 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +164 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/create/.env.example +8 -0
- package/src/create/README.md +101 -0
- package/src/create/compose.yml +14 -0
- package/src/create/domain.ts +153 -0
- package/src/create/package.json +24 -0
- package/src/create/server.ts +18 -0
- package/src/create/setup.sh +11 -0
- package/src/create/tsconfig.json +15 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
|
@@ -1,890 +0,0 @@
|
|
|
1
|
-
-- DZQL Core Functions - Version 3.0.0
|
|
2
|
-
-- Helper functions, utilities, and core DZQL functionality
|
|
3
|
-
|
|
4
|
-
-- === JSON Helpers ===
|
|
5
|
-
CREATE OR REPLACE FUNCTION dzql.jarr(x anyarray)
|
|
6
|
-
RETURNS jsonb LANGUAGE sql IMMUTABLE AS $$
|
|
7
|
-
SELECT coalesce(to_jsonb(x), '[]'::jsonb);
|
|
8
|
-
$$;
|
|
9
|
-
|
|
10
|
-
-- Convert record to jsonb efficiently
|
|
11
|
-
CREATE OR REPLACE FUNCTION dzql.to_jsonb(rec anyelement)
|
|
12
|
-
RETURNS jsonb LANGUAGE plpgsql IMMUTABLE AS $$
|
|
13
|
-
BEGIN
|
|
14
|
-
RETURN to_jsonb(rec);
|
|
15
|
-
END $$;
|
|
16
|
-
|
|
17
|
-
-- Extract object keys as array
|
|
18
|
-
CREATE OR REPLACE FUNCTION dzql.keys(obj jsonb)
|
|
19
|
-
RETURNS text[] LANGUAGE sql IMMUTABLE AS $$
|
|
20
|
-
SELECT array_agg(key) FROM jsonb_object_keys(obj) AS key;
|
|
21
|
-
$$;
|
|
22
|
-
|
|
23
|
-
-- Build JSON object from key-value pair
|
|
24
|
-
CREATE OR REPLACE FUNCTION dzql.j(k text, v jsonb)
|
|
25
|
-
RETURNS jsonb LANGUAGE sql IMMUTABLE AS $$
|
|
26
|
-
SELECT jsonb_build_object(k, v);
|
|
27
|
-
$$;
|
|
28
|
-
|
|
29
|
-
-- Merge multiple JSONB objects
|
|
30
|
-
CREATE OR REPLACE FUNCTION dzql.merge(variadic parts jsonb[])
|
|
31
|
-
RETURNS jsonb LANGUAGE sql IMMUTABLE AS $$
|
|
32
|
-
SELECT coalesce(jsonb_strip_nulls(jsonb_object_agg(k, v)), '{}'::jsonb)
|
|
33
|
-
FROM (
|
|
34
|
-
SELECT (t.parts).key as k, (t.parts).value as v
|
|
35
|
-
FROM (SELECT jsonb_each(coalesce(p,'{}'::jsonb)) parts FROM unnest(parts) p) t
|
|
36
|
-
) s;
|
|
37
|
-
$$;
|
|
38
|
-
|
|
39
|
-
-- === Temporal Filtering Helper ===
|
|
40
|
-
CREATE OR REPLACE FUNCTION dzql.apply_temporal_filter(
|
|
41
|
-
p_table regclass,
|
|
42
|
-
p_temporal_fields jsonb,
|
|
43
|
-
p_on_date timestamptz DEFAULT NULL
|
|
44
|
-
) RETURNS text
|
|
45
|
-
LANGUAGE plpgsql AS $$
|
|
46
|
-
DECLARE
|
|
47
|
-
l_valid_from text;
|
|
48
|
-
l_valid_to text;
|
|
49
|
-
l_filter_date timestamptz;
|
|
50
|
-
BEGIN
|
|
51
|
-
IF p_temporal_fields IS NULL OR p_temporal_fields = '{}' THEN
|
|
52
|
-
RETURN '';
|
|
53
|
-
END IF;
|
|
54
|
-
|
|
55
|
-
l_valid_from := p_temporal_fields->>'valid_from';
|
|
56
|
-
l_valid_to := p_temporal_fields->>'valid_to';
|
|
57
|
-
l_filter_date := coalesce(p_on_date, now());
|
|
58
|
-
|
|
59
|
-
IF l_valid_from IS NULL THEN
|
|
60
|
-
RETURN '';
|
|
61
|
-
END IF;
|
|
62
|
-
|
|
63
|
-
RETURN format(' AND %I <= %L AND (%I > %L OR %I IS NULL)',
|
|
64
|
-
l_valid_from, l_filter_date,
|
|
65
|
-
l_valid_to, l_filter_date, l_valid_to
|
|
66
|
-
);
|
|
67
|
-
END $$;
|
|
68
|
-
|
|
69
|
-
-- === Describe (introspection) ===
|
|
70
|
-
CREATE OR REPLACE FUNCTION dzql.describe()
|
|
71
|
-
RETURNS TABLE(
|
|
72
|
-
table_name text,
|
|
73
|
-
label_field text,
|
|
74
|
-
searchable_fields text[],
|
|
75
|
-
fk_includes jsonb,
|
|
76
|
-
temporal_fields jsonb,
|
|
77
|
-
notification_paths jsonb,
|
|
78
|
-
permission_paths jsonb,
|
|
79
|
-
graph_rules jsonb
|
|
80
|
-
) LANGUAGE sql AS $$
|
|
81
|
-
SELECT e.table_name, e.label_field, e.searchable_fields, e.fk_includes,
|
|
82
|
-
e.temporal_fields, e.notification_paths, e.permission_paths, e.graph_rules
|
|
83
|
-
FROM dzql.entities e
|
|
84
|
-
ORDER BY e.table_name;
|
|
85
|
-
$$;
|
|
86
|
-
|
|
87
|
-
-- === Legacy Exec Dispatcher (for custom functions) ===
|
|
88
|
-
CREATE OR REPLACE FUNCTION dzql.exec(
|
|
89
|
-
exposed text,
|
|
90
|
-
args jsonb DEFAULT '{}',
|
|
91
|
-
p_user_id int DEFAULT NULL
|
|
92
|
-
) RETURNS jsonb
|
|
93
|
-
LANGUAGE plpgsql
|
|
94
|
-
SECURITY INVOKER
|
|
95
|
-
AS $$
|
|
96
|
-
DECLARE
|
|
97
|
-
l_fn_regproc regproc;
|
|
98
|
-
l_result jsonb;
|
|
99
|
-
l_arg_names text[];
|
|
100
|
-
l_arg_values text[];
|
|
101
|
-
l_call_sql text;
|
|
102
|
-
l_key text;
|
|
103
|
-
l_value jsonb;
|
|
104
|
-
l_user_id int;
|
|
105
|
-
BEGIN
|
|
106
|
-
-- Get user_id from session if not provided
|
|
107
|
-
l_user_id := coalesce(p_user_id, nullif(current_setting('dzql.user_id', true), '')::int);
|
|
108
|
-
|
|
109
|
-
-- Check if function is registered
|
|
110
|
-
SELECT fn_regproc INTO l_fn_regproc FROM dzql.registry WHERE fn_regproc = exposed::regproc;
|
|
111
|
-
IF l_fn_regproc IS NULL THEN
|
|
112
|
-
RAISE EXCEPTION 'Function % not found or not registered', exposed;
|
|
113
|
-
END IF;
|
|
114
|
-
|
|
115
|
-
-- Build function call with user_id as first parameter
|
|
116
|
-
l_arg_names := array['p_user_id'];
|
|
117
|
-
l_arg_values := array[coalesce(l_user_id, 0)::text];
|
|
118
|
-
|
|
119
|
-
-- Add other parameters
|
|
120
|
-
IF args IS NOT NULL AND args != '{}' THEN
|
|
121
|
-
FOR l_key, l_value IN SELECT key, value FROM jsonb_each(args)
|
|
122
|
-
LOOP
|
|
123
|
-
l_arg_names := l_arg_names || ('p_' || l_key);
|
|
124
|
-
CASE jsonb_typeof(l_value)
|
|
125
|
-
WHEN 'string' THEN
|
|
126
|
-
l_arg_values := l_arg_values || (l_value #>> '{}');
|
|
127
|
-
WHEN 'number' THEN
|
|
128
|
-
l_arg_values := l_arg_values || (l_value #>> '{}');
|
|
129
|
-
WHEN 'boolean' THEN
|
|
130
|
-
l_arg_values := l_arg_values || (l_value #>> '{}');
|
|
131
|
-
WHEN 'null' THEN
|
|
132
|
-
l_arg_values := l_arg_values || 'NULL';
|
|
133
|
-
ELSE
|
|
134
|
-
l_arg_values := l_arg_values || (l_value::text);
|
|
135
|
-
END CASE;
|
|
136
|
-
END LOOP;
|
|
137
|
-
END IF;
|
|
138
|
-
|
|
139
|
-
-- Build and execute function call
|
|
140
|
-
l_call_sql := format('SELECT %I(%s)', exposed, array_to_string(l_arg_values, ','));
|
|
141
|
-
|
|
142
|
-
BEGIN
|
|
143
|
-
EXECUTE l_call_sql INTO l_result;
|
|
144
|
-
EXCEPTION
|
|
145
|
-
WHEN OTHERS THEN
|
|
146
|
-
RAISE EXCEPTION 'DZQL: exec error for %: %', exposed, SQLERRM;
|
|
147
|
-
END;
|
|
148
|
-
|
|
149
|
-
RETURN l_result;
|
|
150
|
-
END $$;
|
|
151
|
-
|
|
152
|
-
-- === Condition Evaluation ===
|
|
153
|
-
-- Evaluates a condition string with variable substitution
|
|
154
|
-
CREATE OR REPLACE FUNCTION dzql.evaluate_condition(
|
|
155
|
-
p_condition text,
|
|
156
|
-
p_record_before jsonb,
|
|
157
|
-
p_record_after jsonb,
|
|
158
|
-
p_user_id int
|
|
159
|
-
) RETURNS boolean
|
|
160
|
-
LANGUAGE plpgsql AS $$
|
|
161
|
-
DECLARE
|
|
162
|
-
l_resolved_condition text;
|
|
163
|
-
l_result boolean;
|
|
164
|
-
l_match_array text[];
|
|
165
|
-
l_field_name text;
|
|
166
|
-
l_field_value text;
|
|
167
|
-
BEGIN
|
|
168
|
-
-- Replace variables in condition
|
|
169
|
-
l_resolved_condition := p_condition;
|
|
170
|
-
|
|
171
|
-
-- Replace @before.field references
|
|
172
|
-
IF p_record_before IS NOT NULL THEN
|
|
173
|
-
LOOP
|
|
174
|
-
l_match_array := regexp_match(l_resolved_condition, '@before\.(\w+)');
|
|
175
|
-
EXIT WHEN l_match_array IS NULL;
|
|
176
|
-
|
|
177
|
-
l_field_name := l_match_array[1];
|
|
178
|
-
l_field_value := COALESCE(p_record_before->>l_field_name, 'null');
|
|
179
|
-
l_resolved_condition := regexp_replace(
|
|
180
|
-
l_resolved_condition,
|
|
181
|
-
'@before\.' || l_field_name,
|
|
182
|
-
quote_literal(l_field_value),
|
|
183
|
-
1
|
|
184
|
-
);
|
|
185
|
-
END LOOP;
|
|
186
|
-
END IF;
|
|
187
|
-
|
|
188
|
-
-- Replace @after.field references
|
|
189
|
-
IF p_record_after IS NOT NULL THEN
|
|
190
|
-
LOOP
|
|
191
|
-
l_match_array := regexp_match(l_resolved_condition, '@after\.(\w+)');
|
|
192
|
-
EXIT WHEN l_match_array IS NULL;
|
|
193
|
-
|
|
194
|
-
l_field_name := l_match_array[1];
|
|
195
|
-
l_field_value := COALESCE(p_record_after->>l_field_name, 'null');
|
|
196
|
-
l_resolved_condition := regexp_replace(
|
|
197
|
-
l_resolved_condition,
|
|
198
|
-
'@after\.' || l_field_name,
|
|
199
|
-
quote_literal(l_field_value),
|
|
200
|
-
1
|
|
201
|
-
);
|
|
202
|
-
END LOOP;
|
|
203
|
-
END IF;
|
|
204
|
-
|
|
205
|
-
-- Replace @user_id
|
|
206
|
-
l_resolved_condition := replace(l_resolved_condition, '@user_id', p_user_id::text);
|
|
207
|
-
|
|
208
|
-
-- Replace @id (use after if available, otherwise before)
|
|
209
|
-
IF p_record_after IS NOT NULL THEN
|
|
210
|
-
l_resolved_condition := replace(l_resolved_condition, '@id',
|
|
211
|
-
COALESCE(p_record_after->>'id', 'null'));
|
|
212
|
-
ELSIF p_record_before IS NOT NULL THEN
|
|
213
|
-
l_resolved_condition := replace(l_resolved_condition, '@id',
|
|
214
|
-
COALESCE(p_record_before->>'id', 'null'));
|
|
215
|
-
END IF;
|
|
216
|
-
|
|
217
|
-
-- Execute the condition
|
|
218
|
-
BEGIN
|
|
219
|
-
EXECUTE 'SELECT (' || l_resolved_condition || ')::boolean' INTO l_result;
|
|
220
|
-
RETURN COALESCE(l_result, false);
|
|
221
|
-
EXCEPTION WHEN OTHERS THEN
|
|
222
|
-
-- If condition evaluation fails, log and return false
|
|
223
|
-
RAISE WARNING 'Graph rule condition evaluation failed: % (condition: %)', SQLERRM, l_resolved_condition;
|
|
224
|
-
RETURN false;
|
|
225
|
-
END;
|
|
226
|
-
END $$;
|
|
227
|
-
|
|
228
|
-
-- === Path Resolution Functions ===
|
|
229
|
-
-- Resolve notification/permission paths to user IDs
|
|
230
|
-
-- === Path Resolution Functions ===
|
|
231
|
-
-- Resolve a single path segment (handles @field, table[condition], and FK traversal)
|
|
232
|
-
CREATE OR REPLACE FUNCTION dzql.resolve_path_segment(
|
|
233
|
-
p_table_name text,
|
|
234
|
-
p_record jsonb,
|
|
235
|
-
p_segment text
|
|
236
|
-
) RETURNS jsonb
|
|
237
|
-
LANGUAGE plpgsql AS $$
|
|
238
|
-
DECLARE
|
|
239
|
-
l_result jsonb;
|
|
240
|
-
l_parts text[];
|
|
241
|
-
l_field_name text;
|
|
242
|
-
l_table_name text;
|
|
243
|
-
l_condition text;
|
|
244
|
-
l_is_active boolean := false;
|
|
245
|
-
l_sql text;
|
|
246
|
-
l_current_record jsonb;
|
|
247
|
-
l_step text;
|
|
248
|
-
l_hops text[];
|
|
249
|
-
l_final_field text;
|
|
250
|
-
BEGIN
|
|
251
|
-
-- Handle simple field reference: @field_name
|
|
252
|
-
IF p_segment LIKE '@%' THEN
|
|
253
|
-
l_field_name := substring(p_segment from 2);
|
|
254
|
-
IF p_record ? l_field_name AND p_record->>l_field_name IS NOT NULL THEN
|
|
255
|
-
DECLARE
|
|
256
|
-
l_field_value text;
|
|
257
|
-
BEGIN
|
|
258
|
-
l_field_value := p_record->>l_field_name;
|
|
259
|
-
-- Try to convert to integer and return as array
|
|
260
|
-
RETURN to_jsonb(array[l_field_value::int]);
|
|
261
|
-
EXCEPTION WHEN OTHERS THEN
|
|
262
|
-
-- If conversion fails, return as text array
|
|
263
|
-
RETURN to_jsonb(array[l_field_value]);
|
|
264
|
-
END;
|
|
265
|
-
END IF;
|
|
266
|
-
RETURN null;
|
|
267
|
-
END IF;
|
|
268
|
-
|
|
269
|
-
-- Handle conditional queries: table[condition]{active}.field
|
|
270
|
-
IF p_segment ~ '\[.*\]' THEN
|
|
271
|
-
-- Extract parts: table[condition]{active}.field
|
|
272
|
-
l_table_name := split_part(p_segment, '[', 1);
|
|
273
|
-
l_condition := split_part(split_part(p_segment, '[', 2), ']', 1);
|
|
274
|
-
-- Get field name after the closing bracket
|
|
275
|
-
l_step := split_part(p_segment, ']', 2);
|
|
276
|
-
-- Remove {active} if present
|
|
277
|
-
l_step := replace(l_step, '{active}', '');
|
|
278
|
-
-- Get field name after the dot
|
|
279
|
-
l_field_name := ltrim(l_step, '.');
|
|
280
|
-
l_is_active := p_segment LIKE '%{active}%';
|
|
281
|
-
|
|
282
|
-
-- Replace @ references in condition with actual values from the record (if we have one)
|
|
283
|
-
IF p_record IS NOT NULL THEN
|
|
284
|
-
DECLARE
|
|
285
|
-
l_ref_field text;
|
|
286
|
-
BEGIN
|
|
287
|
-
WHILE l_condition ~ '@[a-z_]+' LOOP
|
|
288
|
-
l_ref_field := substring(l_condition from '@([a-z_]+)');
|
|
289
|
-
l_condition := replace(l_condition, '@' || l_ref_field,
|
|
290
|
-
quote_literal(p_record->>l_ref_field));
|
|
291
|
-
END LOOP;
|
|
292
|
-
END;
|
|
293
|
-
END IF;
|
|
294
|
-
|
|
295
|
-
-- Build and execute query
|
|
296
|
-
l_sql := format('SELECT array_agg(%I) FROM %I WHERE %s',
|
|
297
|
-
l_field_name, l_table_name, l_condition);
|
|
298
|
-
|
|
299
|
-
-- Add temporal filtering if {active}
|
|
300
|
-
IF l_is_active THEN
|
|
301
|
-
-- Get temporal field configuration for this table
|
|
302
|
-
DECLARE
|
|
303
|
-
l_temporal_config record;
|
|
304
|
-
l_valid_from_field text;
|
|
305
|
-
l_valid_to_field text;
|
|
306
|
-
BEGIN
|
|
307
|
-
SELECT temporal_fields INTO l_temporal_config
|
|
308
|
-
FROM dzql.entities
|
|
309
|
-
WHERE table_name = l_table_name;
|
|
310
|
-
|
|
311
|
-
IF l_temporal_config.temporal_fields IS NOT NULL AND l_temporal_config.temporal_fields != '{}' THEN
|
|
312
|
-
l_valid_from_field := l_temporal_config.temporal_fields->>'valid_from';
|
|
313
|
-
l_valid_to_field := l_temporal_config.temporal_fields->>'valid_to';
|
|
314
|
-
|
|
315
|
-
IF l_valid_from_field IS NOT NULL THEN
|
|
316
|
-
l_sql := l_sql || format(' AND %I <= now() AND (%I > now() OR %I IS NULL)',
|
|
317
|
-
l_valid_from_field, l_valid_to_field, l_valid_to_field);
|
|
318
|
-
END IF;
|
|
319
|
-
END IF;
|
|
320
|
-
END;
|
|
321
|
-
END IF;
|
|
322
|
-
|
|
323
|
-
DECLARE
|
|
324
|
-
l_ids int[];
|
|
325
|
-
BEGIN
|
|
326
|
-
EXECUTE l_sql INTO l_ids;
|
|
327
|
-
IF l_ids IS NOT NULL THEN
|
|
328
|
-
RETURN to_jsonb(l_ids);
|
|
329
|
-
ELSE
|
|
330
|
-
RETURN null;
|
|
331
|
-
END IF;
|
|
332
|
-
END;
|
|
333
|
-
END IF;
|
|
334
|
-
|
|
335
|
-
-- Handle foreign key traversal (single or multi-hop)
|
|
336
|
-
IF p_segment ~ '\.' AND p_record IS NOT NULL THEN
|
|
337
|
-
-- For paths like site_id.venue_id.org_id, we need to:
|
|
338
|
-
-- 1. Follow site_id to get the site record
|
|
339
|
-
-- 2. Follow venue_id from that to get the venue record
|
|
340
|
-
-- 3. Extract org_id from the venue record
|
|
341
|
-
|
|
342
|
-
l_current_record := p_record;
|
|
343
|
-
l_parts := string_to_array(p_segment, '.');
|
|
344
|
-
|
|
345
|
-
-- We need to track which table we're currently in for FK resolution
|
|
346
|
-
DECLARE
|
|
347
|
-
l_current_table_name text;
|
|
348
|
-
BEGIN
|
|
349
|
-
l_current_table_name := p_table_name;
|
|
350
|
-
|
|
351
|
-
-- Process each part except the last (which is the field to extract)
|
|
352
|
-
FOR i IN 1..(array_length(l_parts, 1) - 1) LOOP
|
|
353
|
-
l_step := l_parts[i];
|
|
354
|
-
|
|
355
|
-
-- If current record has this field, follow it as a foreign key
|
|
356
|
-
IF l_current_record ? l_step THEN
|
|
357
|
-
-- Get the FK value
|
|
358
|
-
DECLARE
|
|
359
|
-
l_fk_value text;
|
|
360
|
-
l_fk_sql text;
|
|
361
|
-
l_entity_config record;
|
|
362
|
-
l_fk_target text;
|
|
363
|
-
BEGIN
|
|
364
|
-
l_fk_value := l_current_record->>l_step;
|
|
365
|
-
|
|
366
|
-
-- Look up the current table's FK configuration
|
|
367
|
-
IF l_current_table_name IS NOT NULL THEN
|
|
368
|
-
SELECT fk_includes INTO l_entity_config
|
|
369
|
-
FROM dzql.entities
|
|
370
|
-
WHERE table_name = l_current_table_name;
|
|
371
|
-
|
|
372
|
-
IF l_entity_config.fk_includes IS NOT NULL AND l_entity_config.fk_includes ? l_step THEN
|
|
373
|
-
-- The fk_includes tells us which table this FK points to
|
|
374
|
-
l_table_name := l_entity_config.fk_includes->>l_step;
|
|
375
|
-
ELSIF l_entity_config.fk_includes IS NOT NULL THEN
|
|
376
|
-
-- Check if the field without _id suffix is in fk_includes
|
|
377
|
-
l_fk_target := regexp_replace(l_step, '_id$', '');
|
|
378
|
-
IF l_entity_config.fk_includes ? l_fk_target THEN
|
|
379
|
-
l_table_name := l_entity_config.fk_includes->>l_fk_target;
|
|
380
|
-
END IF;
|
|
381
|
-
END IF;
|
|
382
|
-
END IF;
|
|
383
|
-
|
|
384
|
-
-- If we still don't have a table name, try pattern matching
|
|
385
|
-
IF l_table_name IS NULL THEN
|
|
386
|
-
DECLARE
|
|
387
|
-
l_pattern text;
|
|
388
|
-
BEGIN
|
|
389
|
-
-- Remove _id suffix and try to find a matching table
|
|
390
|
-
l_pattern := regexp_replace(l_step, '_id$', '');
|
|
391
|
-
|
|
392
|
-
-- Check for common patterns
|
|
393
|
-
IF EXISTS(SELECT 1 FROM dzql.entities WHERE table_name = l_pattern || 's') THEN
|
|
394
|
-
l_table_name := l_pattern || 's';
|
|
395
|
-
ELSIF EXISTS(SELECT 1 FROM dzql.entities WHERE table_name = l_pattern) THEN
|
|
396
|
-
l_table_name := l_pattern;
|
|
397
|
-
END IF;
|
|
398
|
-
END;
|
|
399
|
-
END IF;
|
|
400
|
-
|
|
401
|
-
IF l_table_name IS NULL THEN
|
|
402
|
-
RAISE WARNING 'Could not determine target table for FK field % in table %', l_step, coalesce(l_current_table_name, 'unknown');
|
|
403
|
-
RETURN null;
|
|
404
|
-
END IF;
|
|
405
|
-
|
|
406
|
-
-- Query the related table
|
|
407
|
-
l_fk_sql := format('SELECT to_jsonb(t.*) FROM %I t WHERE t.id = %L', l_table_name, l_fk_value);
|
|
408
|
-
EXECUTE l_fk_sql INTO l_current_record;
|
|
409
|
-
|
|
410
|
-
IF l_current_record IS NULL THEN
|
|
411
|
-
RETURN null; -- FK broken
|
|
412
|
-
END IF;
|
|
413
|
-
|
|
414
|
-
-- Update current table context for next iteration
|
|
415
|
-
l_current_table_name := l_table_name;
|
|
416
|
-
END;
|
|
417
|
-
ELSE
|
|
418
|
-
RETURN null; -- Field not found
|
|
419
|
-
END IF;
|
|
420
|
-
END LOOP;
|
|
421
|
-
END;
|
|
422
|
-
|
|
423
|
-
-- Extract the final field value
|
|
424
|
-
l_final_field := l_parts[array_length(l_parts, 1)];
|
|
425
|
-
IF l_current_record ? l_final_field THEN
|
|
426
|
-
RETURN to_jsonb(array[l_current_record->>l_final_field]::int[]);
|
|
427
|
-
END IF;
|
|
428
|
-
|
|
429
|
-
RETURN null;
|
|
430
|
-
END IF;
|
|
431
|
-
|
|
432
|
-
RETURN null;
|
|
433
|
-
END $$;
|
|
434
|
-
|
|
435
|
-
-- Resolve notification/permission paths to user IDs
|
|
436
|
-
CREATE OR REPLACE FUNCTION dzql.resolve_notification_path(
|
|
437
|
-
p_table_name text,
|
|
438
|
-
p_record jsonb,
|
|
439
|
-
p_path text
|
|
440
|
-
) RETURNS int[]
|
|
441
|
-
LANGUAGE plpgsql AS $$
|
|
442
|
-
DECLARE
|
|
443
|
-
l_result_ids int[] := '{}';
|
|
444
|
-
l_continuation_parts text[];
|
|
445
|
-
l_current_segment text;
|
|
446
|
-
l_current_result jsonb;
|
|
447
|
-
l_current_ids int[];
|
|
448
|
-
l_sql text;
|
|
449
|
-
l_field_name text;
|
|
450
|
-
l_table_name text;
|
|
451
|
-
l_condition text;
|
|
452
|
-
l_is_active boolean := false;
|
|
453
|
-
l_i int;
|
|
454
|
-
BEGIN
|
|
455
|
-
-- Split by continuation operator (->) if present
|
|
456
|
-
IF p_path ~ '->' THEN
|
|
457
|
-
l_continuation_parts := string_to_array(p_path, '->');
|
|
458
|
-
|
|
459
|
-
-- Process first segment with the original record
|
|
460
|
-
l_current_result := dzql.resolve_path_segment(p_table_name, p_record, l_continuation_parts[1]);
|
|
461
|
-
|
|
462
|
-
-- Process each continuation segment
|
|
463
|
-
FOR l_i IN 2..array_length(l_continuation_parts, 1) LOOP
|
|
464
|
-
l_current_segment := l_continuation_parts[l_i];
|
|
465
|
-
|
|
466
|
-
-- Replace $ with the result from previous segment
|
|
467
|
-
IF l_current_result IS NOT NULL THEN
|
|
468
|
-
-- Handle array results
|
|
469
|
-
IF jsonb_typeof(l_current_result) = 'array' THEN
|
|
470
|
-
-- For each ID in the array, resolve the path and collect results
|
|
471
|
-
l_current_ids := '{}';
|
|
472
|
-
DECLARE
|
|
473
|
-
l_j int;
|
|
474
|
-
l_temp_segment text;
|
|
475
|
-
l_temp_ids int[];
|
|
476
|
-
l_temp_value int;
|
|
477
|
-
l_segment_table text;
|
|
478
|
-
l_segment_record jsonb;
|
|
479
|
-
l_replacement_value text;
|
|
480
|
-
BEGIN
|
|
481
|
-
FOR l_j IN 0..jsonb_array_length(l_current_result) - 1 LOOP
|
|
482
|
-
l_replacement_value := (l_current_result->>l_j)::text;
|
|
483
|
-
|
|
484
|
-
-- Check if $ appears in a condition context table[field=$]
|
|
485
|
-
-- If so, we need to quote it properly for text values
|
|
486
|
-
IF l_continuation_parts[l_i] ~ '\[.*\$.*\]' THEN
|
|
487
|
-
-- Try to parse as integer first
|
|
488
|
-
BEGIN
|
|
489
|
-
-- If it's an integer, use it directly
|
|
490
|
-
l_temp_segment := replace(l_continuation_parts[l_i], '$', l_replacement_value::int::text);
|
|
491
|
-
EXCEPTION WHEN OTHERS THEN
|
|
492
|
-
-- Not an integer, quote it as a literal
|
|
493
|
-
l_temp_segment := replace(l_continuation_parts[l_i], '$', quote_literal(l_replacement_value));
|
|
494
|
-
END;
|
|
495
|
-
ELSE
|
|
496
|
-
-- Not in a condition, use raw value
|
|
497
|
-
l_temp_segment := replace(l_continuation_parts[l_i], '$', l_replacement_value);
|
|
498
|
-
END IF;
|
|
499
|
-
|
|
500
|
-
-- Handle table.field syntax in continuation segments
|
|
501
|
-
IF l_temp_segment ~ '^[a-z_]+\.[a-z_]+' THEN
|
|
502
|
-
l_segment_table := split_part(l_temp_segment, '.', 1);
|
|
503
|
-
l_field_name := split_part(l_temp_segment, '.', 2);
|
|
504
|
-
-- Query the table directly to get the field value
|
|
505
|
-
l_sql := format('SELECT %I FROM %I WHERE id = %L', l_field_name, l_segment_table, (l_current_result->>l_j)::int);
|
|
506
|
-
EXECUTE l_sql INTO l_temp_value;
|
|
507
|
-
l_temp_ids := array[l_temp_value];
|
|
508
|
-
ELSE
|
|
509
|
-
l_temp_ids := array(SELECT jsonb_array_elements_text(dzql.resolve_path_segment(null, null, l_temp_segment))::int);
|
|
510
|
-
END IF;
|
|
511
|
-
|
|
512
|
-
l_current_ids := l_current_ids || coalesce(l_temp_ids, '{}');
|
|
513
|
-
END LOOP;
|
|
514
|
-
END;
|
|
515
|
-
l_current_result := to_jsonb(l_current_ids);
|
|
516
|
-
ELSE
|
|
517
|
-
-- Single value result
|
|
518
|
-
DECLARE
|
|
519
|
-
l_replacement_value text;
|
|
520
|
-
BEGIN
|
|
521
|
-
l_replacement_value := l_current_result::text;
|
|
522
|
-
|
|
523
|
-
-- Check if $ appears in a condition context table[field=$]
|
|
524
|
-
-- If so, we need to quote it properly for text values
|
|
525
|
-
IF l_current_segment ~ '\[.*\$.*\]' THEN
|
|
526
|
-
-- Try to parse as integer first
|
|
527
|
-
BEGIN
|
|
528
|
-
-- If it's an integer, use it directly
|
|
529
|
-
l_current_segment := replace(l_current_segment, '$', l_replacement_value::int::text);
|
|
530
|
-
EXCEPTION WHEN OTHERS THEN
|
|
531
|
-
-- Not an integer, quote it as a literal
|
|
532
|
-
l_current_segment := replace(l_current_segment, '$', quote_literal(l_replacement_value));
|
|
533
|
-
END;
|
|
534
|
-
ELSE
|
|
535
|
-
-- Not in a condition, use raw value
|
|
536
|
-
l_current_segment := replace(l_current_segment, '$', l_replacement_value);
|
|
537
|
-
END IF;
|
|
538
|
-
END;
|
|
539
|
-
|
|
540
|
-
-- Handle table.field syntax in continuation segments
|
|
541
|
-
IF l_current_segment ~ '^[a-z_]+\.[a-z_]+' THEN
|
|
542
|
-
l_table_name := split_part(l_current_segment, '.', 1);
|
|
543
|
-
l_field_name := split_part(l_current_segment, '.', 2);
|
|
544
|
-
-- Query the table directly to get the field value
|
|
545
|
-
DECLARE
|
|
546
|
-
l_field_value int;
|
|
547
|
-
BEGIN
|
|
548
|
-
l_sql := format('SELECT %I FROM %I WHERE id = %L', l_field_name, l_table_name, l_current_result::text::int);
|
|
549
|
-
EXECUTE l_sql INTO l_field_value;
|
|
550
|
-
l_current_result := to_jsonb(array[l_field_value]);
|
|
551
|
-
END;
|
|
552
|
-
ELSE
|
|
553
|
-
l_current_result := dzql.resolve_path_segment(null, null, l_current_segment);
|
|
554
|
-
END IF;
|
|
555
|
-
END IF;
|
|
556
|
-
ELSE
|
|
557
|
-
RETURN '{}'; -- Path broken
|
|
558
|
-
END IF;
|
|
559
|
-
END LOOP;
|
|
560
|
-
|
|
561
|
-
-- Convert final result to int array
|
|
562
|
-
IF jsonb_typeof(l_current_result) = 'array' THEN
|
|
563
|
-
l_result_ids := array(SELECT jsonb_array_elements_text(l_current_result)::int);
|
|
564
|
-
ELSE
|
|
565
|
-
l_result_ids := array[l_current_result::text::int];
|
|
566
|
-
END IF;
|
|
567
|
-
|
|
568
|
-
RETURN coalesce(l_result_ids, '{}');
|
|
569
|
-
ELSE
|
|
570
|
-
-- No continuation, process as single segment
|
|
571
|
-
l_current_result := dzql.resolve_path_segment(p_table_name, p_record, p_path);
|
|
572
|
-
|
|
573
|
-
-- Convert result to int array
|
|
574
|
-
IF l_current_result IS NOT NULL THEN
|
|
575
|
-
IF jsonb_typeof(l_current_result) = 'array' THEN
|
|
576
|
-
l_result_ids := array(SELECT jsonb_array_elements_text(l_current_result)::int);
|
|
577
|
-
ELSIF l_current_result::text != 'null' THEN
|
|
578
|
-
l_result_ids := array[l_current_result::text::int];
|
|
579
|
-
END IF;
|
|
580
|
-
END IF;
|
|
581
|
-
|
|
582
|
-
RETURN coalesce(l_result_ids, '{}');
|
|
583
|
-
END IF;
|
|
584
|
-
EXCEPTION
|
|
585
|
-
WHEN others THEN
|
|
586
|
-
RAISE WARNING 'Failed to resolve path %: %', p_path, SQLERRM;
|
|
587
|
-
RETURN '{}';
|
|
588
|
-
END $$;
|
|
589
|
-
|
|
590
|
-
-- Backward compatibility alias
|
|
591
|
-
CREATE OR REPLACE FUNCTION dzql.resolve_path_to_users(
|
|
592
|
-
p_path text,
|
|
593
|
-
p_record jsonb,
|
|
594
|
-
p_table_name text DEFAULT NULL
|
|
595
|
-
) RETURNS int[]
|
|
596
|
-
LANGUAGE plpgsql AS $$
|
|
597
|
-
BEGIN
|
|
598
|
-
RETURN dzql.resolve_notification_path(p_table_name, p_record, p_path);
|
|
599
|
-
END $$;
|
|
600
|
-
|
|
601
|
-
-- === Permission Validation ===
|
|
602
|
-
CREATE OR REPLACE FUNCTION dzql.validate_permission_paths(
|
|
603
|
-
p_table_name text,
|
|
604
|
-
p_permission_paths jsonb
|
|
605
|
-
) RETURNS boolean
|
|
606
|
-
LANGUAGE plpgsql AS $$
|
|
607
|
-
DECLARE
|
|
608
|
-
l_operation text;
|
|
609
|
-
l_paths jsonb;
|
|
610
|
-
BEGIN
|
|
611
|
-
-- Check if permission_paths is valid JSON object
|
|
612
|
-
IF jsonb_typeof(p_permission_paths) != 'object' THEN
|
|
613
|
-
RETURN false;
|
|
614
|
-
END IF;
|
|
615
|
-
|
|
616
|
-
-- Check valid operation keys
|
|
617
|
-
FOR l_operation IN SELECT key FROM jsonb_object_keys(p_permission_paths) AS key
|
|
618
|
-
LOOP
|
|
619
|
-
IF l_operation NOT IN ('create', 'update', 'delete', 'view') THEN
|
|
620
|
-
RAISE WARNING 'Invalid permission operation: %', l_operation;
|
|
621
|
-
RETURN false;
|
|
622
|
-
END IF;
|
|
623
|
-
|
|
624
|
-
l_paths := p_permission_paths->l_operation;
|
|
625
|
-
IF jsonb_typeof(l_paths) != 'array' THEN
|
|
626
|
-
RAISE WARNING 'Permission paths for % must be an array', l_operation;
|
|
627
|
-
RETURN false;
|
|
628
|
-
END IF;
|
|
629
|
-
END LOOP;
|
|
630
|
-
|
|
631
|
-
RETURN true;
|
|
632
|
-
END $$;
|
|
633
|
-
|
|
634
|
-
-- Check if user has permission for operation
|
|
635
|
-
CREATE OR REPLACE FUNCTION dzql.check_permission(
|
|
636
|
-
p_user_id int,
|
|
637
|
-
p_operation text,
|
|
638
|
-
p_entity text,
|
|
639
|
-
p_record jsonb
|
|
640
|
-
) RETURNS boolean
|
|
641
|
-
LANGUAGE plpgsql AS $$
|
|
642
|
-
DECLARE
|
|
643
|
-
l_entity_config record;
|
|
644
|
-
l_permission_paths jsonb;
|
|
645
|
-
l_path text;
|
|
646
|
-
l_allowed_users int[];
|
|
647
|
-
BEGIN
|
|
648
|
-
-- Get entity configuration
|
|
649
|
-
SELECT * INTO l_entity_config FROM dzql.entities WHERE table_name = p_entity;
|
|
650
|
-
|
|
651
|
-
IF l_entity_config IS NULL THEN
|
|
652
|
-
RETURN true; -- Entity not configured - permissive default like old version
|
|
653
|
-
END IF;
|
|
654
|
-
|
|
655
|
-
l_permission_paths := l_entity_config.permission_paths->p_operation;
|
|
656
|
-
|
|
657
|
-
-- Empty array means public access
|
|
658
|
-
IF l_permission_paths IS NULL OR jsonb_array_length(l_permission_paths) = 0 THEN
|
|
659
|
-
RETURN true;
|
|
660
|
-
END IF;
|
|
661
|
-
|
|
662
|
-
-- Check each permission path
|
|
663
|
-
FOR l_path IN SELECT jsonb_array_elements_text(l_permission_paths)
|
|
664
|
-
LOOP
|
|
665
|
-
l_allowed_users := dzql.resolve_notification_path(p_entity, p_record, l_path);
|
|
666
|
-
|
|
667
|
-
IF p_user_id = ANY(l_allowed_users) THEN
|
|
668
|
-
RETURN true;
|
|
669
|
-
END IF;
|
|
670
|
-
END LOOP;
|
|
671
|
-
|
|
672
|
-
RETURN false;
|
|
673
|
-
END $$;
|
|
674
|
-
|
|
675
|
-
-- Resolve all notification paths for an entity to user IDs
|
|
676
|
-
CREATE OR REPLACE FUNCTION dzql.resolve_notification_paths(
|
|
677
|
-
p_table_name text,
|
|
678
|
-
p_record jsonb
|
|
679
|
-
) RETURNS int[]
|
|
680
|
-
LANGUAGE plpgsql AS $$
|
|
681
|
-
DECLARE
|
|
682
|
-
l_entity_config record;
|
|
683
|
-
l_all_user_ids int[] := '{}';
|
|
684
|
-
l_path_group_key text;
|
|
685
|
-
l_path_array jsonb;
|
|
686
|
-
l_path text;
|
|
687
|
-
l_user_ids int[];
|
|
688
|
-
BEGIN
|
|
689
|
-
-- Get entity configuration
|
|
690
|
-
SELECT * INTO l_entity_config
|
|
691
|
-
FROM dzql.entities
|
|
692
|
-
WHERE table_name = p_table_name;
|
|
693
|
-
|
|
694
|
-
IF l_entity_config.notification_paths IS NULL THEN
|
|
695
|
-
RETURN '{}';
|
|
696
|
-
END IF;
|
|
697
|
-
|
|
698
|
-
-- Process each notification path group (ownership, commercial, delegated, etc.)
|
|
699
|
-
FOR l_path_group_key IN SELECT jsonb_object_keys(l_entity_config.notification_paths)
|
|
700
|
-
LOOP
|
|
701
|
-
l_path_array := l_entity_config.notification_paths->l_path_group_key;
|
|
702
|
-
|
|
703
|
-
-- Process each path in the group
|
|
704
|
-
FOR l_path IN SELECT jsonb_array_elements_text(l_path_array)
|
|
705
|
-
LOOP
|
|
706
|
-
l_user_ids := dzql.resolve_notification_path(p_table_name, p_record, l_path);
|
|
707
|
-
l_all_user_ids := l_all_user_ids || l_user_ids;
|
|
708
|
-
END LOOP;
|
|
709
|
-
END LOOP;
|
|
710
|
-
|
|
711
|
-
-- Remove duplicates and return user_ids directly (paths now resolve to user_ids)
|
|
712
|
-
l_all_user_ids := array(SELECT DISTINCT unnest(l_all_user_ids));
|
|
713
|
-
|
|
714
|
-
RETURN coalesce(l_all_user_ids, '{}');
|
|
715
|
-
END $$;
|
|
716
|
-
|
|
717
|
-
-- === Utility Functions ===
|
|
718
|
-
-- Set current user for audit trail
|
|
719
|
-
CREATE OR REPLACE FUNCTION dzql.set_current_user(p_user_id int)
|
|
720
|
-
RETURNS void
|
|
721
|
-
LANGUAGE sql AS $$
|
|
722
|
-
SELECT set_config('app.current_user_id', p_user_id::text, false);
|
|
723
|
-
$$;
|
|
724
|
-
|
|
725
|
-
-- === Graph Rules Helper Functions ===
|
|
726
|
-
-- Validate graph rules structure
|
|
727
|
-
CREATE OR REPLACE FUNCTION dzql.validate_graph_rules(
|
|
728
|
-
p_rules jsonb
|
|
729
|
-
) RETURNS boolean
|
|
730
|
-
LANGUAGE plpgsql AS $$
|
|
731
|
-
DECLARE
|
|
732
|
-
l_trigger_key text;
|
|
733
|
-
l_trigger_rules jsonb;
|
|
734
|
-
l_rule_name text;
|
|
735
|
-
l_rule_config jsonb;
|
|
736
|
-
l_action jsonb;
|
|
737
|
-
l_action_type text;
|
|
738
|
-
BEGIN
|
|
739
|
-
-- Check if rules is empty or null (valid)
|
|
740
|
-
IF p_rules IS NULL OR p_rules = '{}' THEN
|
|
741
|
-
RETURN true;
|
|
742
|
-
END IF;
|
|
743
|
-
|
|
744
|
-
-- Validate top-level trigger types
|
|
745
|
-
FOR l_trigger_key, l_trigger_rules IN SELECT * FROM jsonb_each(p_rules)
|
|
746
|
-
LOOP
|
|
747
|
-
-- Skip validation for many_to_many (different structure)
|
|
748
|
-
IF l_trigger_key = 'many_to_many' THEN
|
|
749
|
-
CONTINUE;
|
|
750
|
-
END IF;
|
|
751
|
-
|
|
752
|
-
-- Check for simpler CASCADE/SET NULL/RESTRICT format: {"delete": {"entity_name": "CASCADE"}}
|
|
753
|
-
IF l_trigger_key IN ('delete', 'update', 'create') AND jsonb_typeof(l_trigger_rules) = 'object' THEN
|
|
754
|
-
-- This is the simpler format - validate CASCADE/SET NULL/RESTRICT values
|
|
755
|
-
DECLARE
|
|
756
|
-
l_entity_name text;
|
|
757
|
-
l_action_value text;
|
|
758
|
-
BEGIN
|
|
759
|
-
FOR l_entity_name, l_action_value IN SELECT * FROM jsonb_each_text(l_trigger_rules)
|
|
760
|
-
LOOP
|
|
761
|
-
IF l_action_value NOT IN ('CASCADE', 'SET NULL', 'RESTRICT') THEN
|
|
762
|
-
RAISE WARNING 'Invalid graph rule action for entity %: %. Must be CASCADE, SET NULL, or RESTRICT', l_entity_name, l_action_value;
|
|
763
|
-
RETURN false;
|
|
764
|
-
END IF;
|
|
765
|
-
END LOOP;
|
|
766
|
-
-- Valid simpler format - skip complex validation
|
|
767
|
-
CONTINUE;
|
|
768
|
-
END;
|
|
769
|
-
END IF;
|
|
770
|
-
|
|
771
|
-
-- Check valid trigger types for complex format
|
|
772
|
-
IF l_trigger_key NOT IN ('on_create', 'on_update', 'on_delete', 'on_field_change') THEN
|
|
773
|
-
RAISE WARNING 'Invalid trigger type: %', l_trigger_key;
|
|
774
|
-
RETURN false;
|
|
775
|
-
END IF;
|
|
776
|
-
|
|
777
|
-
-- Validate each rule within the trigger
|
|
778
|
-
FOR l_rule_name, l_rule_config IN SELECT * FROM jsonb_each(l_trigger_rules)
|
|
779
|
-
LOOP
|
|
780
|
-
-- Check required fields
|
|
781
|
-
IF NOT l_rule_config ? 'actions' THEN
|
|
782
|
-
RAISE WARNING 'Rule % missing required "actions" field', l_rule_name;
|
|
783
|
-
RETURN false;
|
|
784
|
-
END IF;
|
|
785
|
-
|
|
786
|
-
-- Validate each action
|
|
787
|
-
FOR l_action IN SELECT * FROM jsonb_array_elements(l_rule_config->'actions')
|
|
788
|
-
LOOP
|
|
789
|
-
l_action_type := l_action->>'type';
|
|
790
|
-
|
|
791
|
-
-- Check valid action types (includes new 'validate' and 'execute' types)
|
|
792
|
-
IF l_action_type NOT IN ('create', 'update', 'delete', 'validate', 'execute') THEN
|
|
793
|
-
RAISE WARNING 'Invalid action type: %', l_action_type;
|
|
794
|
-
RETURN false;
|
|
795
|
-
END IF;
|
|
796
|
-
|
|
797
|
-
-- Check required fields per action type
|
|
798
|
-
IF l_action_type IN ('create', 'update') AND NOT l_action ? 'entity' THEN
|
|
799
|
-
RAISE WARNING 'Action type % requires "entity" field', l_action_type;
|
|
800
|
-
RETURN false;
|
|
801
|
-
END IF;
|
|
802
|
-
|
|
803
|
-
IF l_action_type = 'create' AND NOT l_action ? 'data' THEN
|
|
804
|
-
RAISE WARNING 'Action type "create" requires "data" field';
|
|
805
|
-
RETURN false;
|
|
806
|
-
END IF;
|
|
807
|
-
|
|
808
|
-
IF l_action_type IN ('update', 'delete') AND NOT l_action ? 'match' THEN
|
|
809
|
-
RAISE WARNING 'Action type % requires "match" field', l_action_type;
|
|
810
|
-
RETURN false;
|
|
811
|
-
END IF;
|
|
812
|
-
|
|
813
|
-
-- Validate new action types
|
|
814
|
-
IF l_action_type IN ('validate', 'execute') AND NOT l_action ? 'function' THEN
|
|
815
|
-
RAISE WARNING 'Action type % requires "function" field', l_action_type;
|
|
816
|
-
RETURN false;
|
|
817
|
-
END IF;
|
|
818
|
-
|
|
819
|
-
IF l_action_type IN ('validate', 'execute') AND NOT l_action ? 'params' THEN
|
|
820
|
-
RAISE WARNING 'Action type % requires "params" field', l_action_type;
|
|
821
|
-
RETURN false;
|
|
822
|
-
END IF;
|
|
823
|
-
END LOOP;
|
|
824
|
-
END LOOP;
|
|
825
|
-
END LOOP;
|
|
826
|
-
|
|
827
|
-
RETURN true;
|
|
828
|
-
END $$;
|
|
829
|
-
|
|
830
|
-
-- Resolve graph variables like @user_id, @field_name, etc.
|
|
831
|
-
CREATE OR REPLACE FUNCTION dzql.resolve_graph_variable(
|
|
832
|
-
p_variable text,
|
|
833
|
-
p_record_before jsonb,
|
|
834
|
-
p_record_after jsonb,
|
|
835
|
-
p_user_id int
|
|
836
|
-
) RETURNS text
|
|
837
|
-
LANGUAGE plpgsql AS $$
|
|
838
|
-
DECLARE
|
|
839
|
-
l_result text;
|
|
840
|
-
l_field_name text;
|
|
841
|
-
l_record jsonb;
|
|
842
|
-
BEGIN
|
|
843
|
-
-- Handle built-in variables
|
|
844
|
-
CASE p_variable
|
|
845
|
-
WHEN '@user_id' THEN
|
|
846
|
-
l_result := p_user_id::text;
|
|
847
|
-
WHEN '@now' THEN
|
|
848
|
-
l_result := now()::text;
|
|
849
|
-
WHEN '@today' THEN
|
|
850
|
-
l_result := current_date::text;
|
|
851
|
-
ELSE
|
|
852
|
-
-- Handle field variables like @field_name or @field_name_before
|
|
853
|
-
IF p_variable LIKE '@%_before' THEN
|
|
854
|
-
l_field_name := substring(p_variable from 2 for length(p_variable) - 8);
|
|
855
|
-
l_record := p_record_before;
|
|
856
|
-
ELSE
|
|
857
|
-
l_field_name := substring(p_variable from 2);
|
|
858
|
-
l_record := COALESCE(p_record_after, p_record_before);
|
|
859
|
-
END IF;
|
|
860
|
-
|
|
861
|
-
l_result := l_record->>l_field_name;
|
|
862
|
-
END CASE;
|
|
863
|
-
|
|
864
|
-
RETURN l_result;
|
|
865
|
-
END $$;
|
|
866
|
-
|
|
867
|
-
-- Resolve data object with variable substitution
|
|
868
|
-
CREATE OR REPLACE FUNCTION dzql.resolve_graph_data(
|
|
869
|
-
p_data jsonb,
|
|
870
|
-
p_record_before jsonb,
|
|
871
|
-
p_record_after jsonb,
|
|
872
|
-
p_user_id int
|
|
873
|
-
) RETURNS jsonb
|
|
874
|
-
LANGUAGE plpgsql AS $$
|
|
875
|
-
DECLARE
|
|
876
|
-
l_result jsonb := '{}';
|
|
877
|
-
l_key text;
|
|
878
|
-
l_value text;
|
|
879
|
-
BEGIN
|
|
880
|
-
FOR l_key, l_value IN SELECT * FROM jsonb_each_text(p_data)
|
|
881
|
-
LOOP
|
|
882
|
-
IF l_value LIKE '@%' THEN
|
|
883
|
-
l_value := dzql.resolve_graph_variable(l_value, p_record_before, p_record_after, p_user_id);
|
|
884
|
-
END IF;
|
|
885
|
-
|
|
886
|
-
l_result := l_result || jsonb_build_object(l_key, l_value);
|
|
887
|
-
END LOOP;
|
|
888
|
-
|
|
889
|
-
RETURN l_result;
|
|
890
|
-
END $$;
|