dzql 0.1.0 → 0.1.1
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/package.json
CHANGED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
-- Graph Rules Extensions: Validation and Custom Function Execution
|
|
2
|
+
-- Adds support for 'validate' and 'execute' action types in graph rules
|
|
3
|
+
|
|
4
|
+
-- === Condition Evaluation ===
|
|
5
|
+
-- Evaluates a condition string with variable substitution
|
|
6
|
+
CREATE OR REPLACE FUNCTION dzql.evaluate_condition(
|
|
7
|
+
p_condition text,
|
|
8
|
+
p_record_before jsonb,
|
|
9
|
+
p_record_after jsonb,
|
|
10
|
+
p_user_id int
|
|
11
|
+
) RETURNS boolean
|
|
12
|
+
LANGUAGE plpgsql AS $$
|
|
13
|
+
DECLARE
|
|
14
|
+
l_resolved_condition text;
|
|
15
|
+
l_result boolean;
|
|
16
|
+
l_match_array text[];
|
|
17
|
+
l_field_name text;
|
|
18
|
+
l_field_value text;
|
|
19
|
+
BEGIN
|
|
20
|
+
-- Replace variables in condition
|
|
21
|
+
l_resolved_condition := p_condition;
|
|
22
|
+
|
|
23
|
+
-- Replace @before.field references
|
|
24
|
+
IF p_record_before IS NOT NULL THEN
|
|
25
|
+
LOOP
|
|
26
|
+
l_match_array := regexp_match(l_resolved_condition, '@before\.(\w+)');
|
|
27
|
+
EXIT WHEN l_match_array IS NULL;
|
|
28
|
+
|
|
29
|
+
l_field_name := l_match_array[1];
|
|
30
|
+
l_field_value := COALESCE(p_record_before->>l_field_name, 'null');
|
|
31
|
+
l_resolved_condition := regexp_replace(
|
|
32
|
+
l_resolved_condition,
|
|
33
|
+
'@before\.' || l_field_name,
|
|
34
|
+
quote_literal(l_field_value),
|
|
35
|
+
1
|
|
36
|
+
);
|
|
37
|
+
END LOOP;
|
|
38
|
+
END IF;
|
|
39
|
+
|
|
40
|
+
-- Replace @after.field references
|
|
41
|
+
IF p_record_after IS NOT NULL THEN
|
|
42
|
+
LOOP
|
|
43
|
+
l_match_array := regexp_match(l_resolved_condition, '@after\.(\w+)');
|
|
44
|
+
EXIT WHEN l_match_array IS NULL;
|
|
45
|
+
|
|
46
|
+
l_field_name := l_match_array[1];
|
|
47
|
+
l_field_value := COALESCE(p_record_after->>l_field_name, 'null');
|
|
48
|
+
l_resolved_condition := regexp_replace(
|
|
49
|
+
l_resolved_condition,
|
|
50
|
+
'@after\.' || l_field_name,
|
|
51
|
+
quote_literal(l_field_value),
|
|
52
|
+
1
|
|
53
|
+
);
|
|
54
|
+
END LOOP;
|
|
55
|
+
END IF;
|
|
56
|
+
|
|
57
|
+
-- Replace @user_id
|
|
58
|
+
l_resolved_condition := replace(l_resolved_condition, '@user_id', p_user_id::text);
|
|
59
|
+
|
|
60
|
+
-- Replace @id (use after if available, otherwise before)
|
|
61
|
+
IF p_record_after IS NOT NULL THEN
|
|
62
|
+
l_resolved_condition := replace(l_resolved_condition, '@id',
|
|
63
|
+
COALESCE(p_record_after->>'id', 'null'));
|
|
64
|
+
ELSIF p_record_before IS NOT NULL THEN
|
|
65
|
+
l_resolved_condition := replace(l_resolved_condition, '@id',
|
|
66
|
+
COALESCE(p_record_before->>'id', 'null'));
|
|
67
|
+
END IF;
|
|
68
|
+
|
|
69
|
+
-- Execute the condition
|
|
70
|
+
BEGIN
|
|
71
|
+
EXECUTE 'SELECT (' || l_resolved_condition || ')::boolean' INTO l_result;
|
|
72
|
+
RETURN COALESCE(l_result, false);
|
|
73
|
+
EXCEPTION WHEN OTHERS THEN
|
|
74
|
+
-- If condition evaluation fails, log and return false
|
|
75
|
+
RAISE WARNING 'Graph rule condition evaluation failed: % (condition: %)', SQLERRM, l_resolved_condition;
|
|
76
|
+
RETURN false;
|
|
77
|
+
END;
|
|
78
|
+
END $$;
|
|
79
|
+
|
|
80
|
+
-- === Validate Action ===
|
|
81
|
+
-- Calls a validation function and raises exception if it returns false
|
|
82
|
+
CREATE OR REPLACE FUNCTION dzql.execute_graph_validate(
|
|
83
|
+
p_function_name text,
|
|
84
|
+
p_params jsonb,
|
|
85
|
+
p_error_message text DEFAULT 'Validation failed'
|
|
86
|
+
) RETURNS void
|
|
87
|
+
LANGUAGE plpgsql AS $$
|
|
88
|
+
DECLARE
|
|
89
|
+
l_result boolean;
|
|
90
|
+
l_sql text;
|
|
91
|
+
l_param_list text[];
|
|
92
|
+
l_key text;
|
|
93
|
+
l_value text;
|
|
94
|
+
BEGIN
|
|
95
|
+
-- Validate function name (prevent SQL injection)
|
|
96
|
+
IF NOT p_function_name ~ '^[a-z_][a-z0-9_]*$' THEN
|
|
97
|
+
RAISE EXCEPTION 'Invalid function name: %', p_function_name;
|
|
98
|
+
END IF;
|
|
99
|
+
|
|
100
|
+
-- Build parameter list
|
|
101
|
+
l_param_list := array[]::text[];
|
|
102
|
+
FOR l_key, l_value IN SELECT * FROM jsonb_each_text(p_params)
|
|
103
|
+
LOOP
|
|
104
|
+
l_param_list := l_param_list || (l_key || ' => ' || quote_literal(l_value));
|
|
105
|
+
END LOOP;
|
|
106
|
+
|
|
107
|
+
-- Build and execute function call
|
|
108
|
+
IF array_length(l_param_list, 1) > 0 THEN
|
|
109
|
+
l_sql := format('SELECT %I(%s)', p_function_name, array_to_string(l_param_list, ', '));
|
|
110
|
+
ELSE
|
|
111
|
+
l_sql := format('SELECT %I()', p_function_name);
|
|
112
|
+
END IF;
|
|
113
|
+
|
|
114
|
+
EXECUTE l_sql INTO l_result;
|
|
115
|
+
|
|
116
|
+
-- Raise exception if validation failed
|
|
117
|
+
IF NOT COALESCE(l_result, false) THEN
|
|
118
|
+
RAISE EXCEPTION '%', p_error_message;
|
|
119
|
+
END IF;
|
|
120
|
+
END $$;
|
|
121
|
+
|
|
122
|
+
-- === Execute Action ===
|
|
123
|
+
-- Calls a custom function with parameters (fire-and-forget)
|
|
124
|
+
CREATE OR REPLACE FUNCTION dzql.execute_graph_function(
|
|
125
|
+
p_function_name text,
|
|
126
|
+
p_params jsonb
|
|
127
|
+
) RETURNS jsonb
|
|
128
|
+
LANGUAGE plpgsql AS $$
|
|
129
|
+
DECLARE
|
|
130
|
+
l_result jsonb;
|
|
131
|
+
l_sql text;
|
|
132
|
+
l_param_list text[];
|
|
133
|
+
l_key text;
|
|
134
|
+
l_value text;
|
|
135
|
+
BEGIN
|
|
136
|
+
-- Validate function name (prevent SQL injection)
|
|
137
|
+
IF NOT p_function_name ~ '^[a-z_][a-z0-9_]*$' THEN
|
|
138
|
+
RAISE EXCEPTION 'Invalid function name: %', p_function_name;
|
|
139
|
+
END IF;
|
|
140
|
+
|
|
141
|
+
-- Build parameter list
|
|
142
|
+
l_param_list := array[]::text[];
|
|
143
|
+
FOR l_key, l_value IN SELECT * FROM jsonb_each_text(p_params)
|
|
144
|
+
LOOP
|
|
145
|
+
l_param_list := l_param_list || (l_key || ' => ' || quote_literal(l_value));
|
|
146
|
+
END LOOP;
|
|
147
|
+
|
|
148
|
+
-- Build and execute function call
|
|
149
|
+
IF array_length(l_param_list, 1) > 0 THEN
|
|
150
|
+
l_sql := format('SELECT %I(%s)', p_function_name, array_to_string(l_param_list, ', '));
|
|
151
|
+
ELSE
|
|
152
|
+
l_sql := format('SELECT %I()', p_function_name);
|
|
153
|
+
END IF;
|
|
154
|
+
|
|
155
|
+
EXECUTE l_sql INTO l_result;
|
|
156
|
+
|
|
157
|
+
RETURN COALESCE(l_result, '{}'::jsonb);
|
|
158
|
+
EXCEPTION WHEN OTHERS THEN
|
|
159
|
+
-- Log error but don't fail the transaction
|
|
160
|
+
RAISE WARNING 'Graph rule function execution failed: % (function: %)', SQLERRM, p_function_name;
|
|
161
|
+
RETURN jsonb_build_object('error', SQLERRM);
|
|
162
|
+
END $$;
|
|
163
|
+
|
|
164
|
+
-- === Update execute_graph_rules to support new action types ===
|
|
165
|
+
CREATE OR REPLACE FUNCTION dzql.execute_graph_rules(
|
|
166
|
+
p_table_name text,
|
|
167
|
+
p_operation text, -- 'insert', 'update', 'delete'
|
|
168
|
+
p_record_before jsonb,
|
|
169
|
+
p_record_after jsonb,
|
|
170
|
+
p_user_id int
|
|
171
|
+
) RETURNS jsonb
|
|
172
|
+
LANGUAGE plpgsql AS $$
|
|
173
|
+
DECLARE
|
|
174
|
+
l_entity_config record;
|
|
175
|
+
l_graph_rules jsonb;
|
|
176
|
+
l_trigger_key text;
|
|
177
|
+
l_trigger_rules jsonb;
|
|
178
|
+
l_rule_name text;
|
|
179
|
+
l_rule_config jsonb;
|
|
180
|
+
l_action jsonb;
|
|
181
|
+
l_action_type text;
|
|
182
|
+
l_target_entity text;
|
|
183
|
+
l_action_data jsonb;
|
|
184
|
+
l_action_match jsonb;
|
|
185
|
+
l_resolved_data jsonb;
|
|
186
|
+
l_resolved_match jsonb;
|
|
187
|
+
l_execution_log jsonb := '[]'::jsonb;
|
|
188
|
+
l_condition text;
|
|
189
|
+
l_condition_result boolean;
|
|
190
|
+
l_function_name text;
|
|
191
|
+
l_function_params jsonb;
|
|
192
|
+
l_error_message text;
|
|
193
|
+
l_function_result jsonb;
|
|
194
|
+
BEGIN
|
|
195
|
+
-- Get entity configuration
|
|
196
|
+
SELECT * INTO l_entity_config FROM dzql.entities WHERE table_name = p_table_name;
|
|
197
|
+
|
|
198
|
+
IF l_entity_config IS NULL THEN
|
|
199
|
+
RETURN jsonb_build_object('status', 'entity_not_found');
|
|
200
|
+
END IF;
|
|
201
|
+
|
|
202
|
+
l_graph_rules := l_entity_config.graph_rules;
|
|
203
|
+
|
|
204
|
+
-- Early exit if no graph rules
|
|
205
|
+
IF l_graph_rules IS NULL OR l_graph_rules = '{}' THEN
|
|
206
|
+
RETURN jsonb_build_object('status', 'no_rules');
|
|
207
|
+
END IF;
|
|
208
|
+
|
|
209
|
+
-- Map operation to trigger key
|
|
210
|
+
l_trigger_key := CASE p_operation
|
|
211
|
+
WHEN 'insert' THEN 'on_create'
|
|
212
|
+
WHEN 'update' THEN 'on_update'
|
|
213
|
+
WHEN 'delete' THEN 'on_delete'
|
|
214
|
+
ELSE NULL
|
|
215
|
+
END;
|
|
216
|
+
|
|
217
|
+
IF l_trigger_key IS NULL THEN
|
|
218
|
+
RETURN jsonb_build_object('status', 'invalid_operation', 'operation', p_operation);
|
|
219
|
+
END IF;
|
|
220
|
+
|
|
221
|
+
-- Get rules for this trigger
|
|
222
|
+
l_trigger_rules := l_graph_rules->l_trigger_key;
|
|
223
|
+
|
|
224
|
+
IF l_trigger_rules IS NULL OR l_trigger_rules = '{}' THEN
|
|
225
|
+
RETURN jsonb_build_object('status', 'no_rules_for_trigger', 'trigger', l_trigger_key);
|
|
226
|
+
END IF;
|
|
227
|
+
|
|
228
|
+
-- Execute each rule
|
|
229
|
+
FOR l_rule_name, l_rule_config IN SELECT * FROM jsonb_each(l_trigger_rules)
|
|
230
|
+
LOOP
|
|
231
|
+
-- Check condition if present
|
|
232
|
+
l_condition := l_rule_config->>'condition';
|
|
233
|
+
l_condition_result := true; -- Default to true if no condition
|
|
234
|
+
|
|
235
|
+
IF l_condition IS NOT NULL THEN
|
|
236
|
+
l_condition_result := dzql.evaluate_condition(l_condition, p_record_before, p_record_after, p_user_id);
|
|
237
|
+
END IF;
|
|
238
|
+
|
|
239
|
+
IF l_condition_result THEN
|
|
240
|
+
-- Execute each action in the rule
|
|
241
|
+
FOR l_action IN SELECT * FROM jsonb_array_elements(l_rule_config->'actions')
|
|
242
|
+
LOOP
|
|
243
|
+
l_action_type := l_action->>'type';
|
|
244
|
+
|
|
245
|
+
-- Execute the action based on type
|
|
246
|
+
BEGIN
|
|
247
|
+
CASE l_action_type
|
|
248
|
+
-- Existing action types
|
|
249
|
+
WHEN 'create' THEN
|
|
250
|
+
l_target_entity := l_action->>'entity';
|
|
251
|
+
l_action_data := l_action->'data';
|
|
252
|
+
l_resolved_data := dzql.resolve_graph_data(l_action_data, p_record_before, p_record_after, p_user_id);
|
|
253
|
+
PERFORM dzql.execute_graph_insert(l_target_entity, l_resolved_data, p_user_id);
|
|
254
|
+
|
|
255
|
+
WHEN 'update' THEN
|
|
256
|
+
l_target_entity := l_action->>'entity';
|
|
257
|
+
l_action_data := l_action->'data';
|
|
258
|
+
l_action_match := l_action->'match';
|
|
259
|
+
l_resolved_data := dzql.resolve_graph_data(l_action_data, p_record_before, p_record_after, p_user_id);
|
|
260
|
+
l_resolved_match := dzql.resolve_graph_data(l_action_match, p_record_before, p_record_after, p_user_id);
|
|
261
|
+
PERFORM dzql.execute_graph_update(l_target_entity, l_resolved_match, l_resolved_data, p_user_id);
|
|
262
|
+
|
|
263
|
+
WHEN 'delete' THEN
|
|
264
|
+
l_target_entity := l_action->>'entity';
|
|
265
|
+
l_action_match := l_action->'match';
|
|
266
|
+
l_resolved_match := dzql.resolve_graph_data(l_action_match, p_record_before, p_record_after, p_user_id);
|
|
267
|
+
PERFORM dzql.execute_graph_delete(l_target_entity, l_resolved_match, p_user_id);
|
|
268
|
+
|
|
269
|
+
-- NEW: Validation action
|
|
270
|
+
WHEN 'validate' THEN
|
|
271
|
+
l_function_name := l_action->>'function';
|
|
272
|
+
l_function_params := l_action->'params';
|
|
273
|
+
l_error_message := COALESCE(l_action->>'error_message', 'Validation failed');
|
|
274
|
+
l_resolved_data := dzql.resolve_graph_data(l_function_params, p_record_before, p_record_after, p_user_id);
|
|
275
|
+
PERFORM dzql.execute_graph_validate(l_function_name, l_resolved_data, l_error_message);
|
|
276
|
+
|
|
277
|
+
-- NEW: Execute function action
|
|
278
|
+
WHEN 'execute' THEN
|
|
279
|
+
l_function_name := l_action->>'function';
|
|
280
|
+
l_function_params := l_action->'params';
|
|
281
|
+
l_resolved_data := dzql.resolve_graph_data(l_function_params, p_record_before, p_record_after, p_user_id);
|
|
282
|
+
l_function_result := dzql.execute_graph_function(l_function_name, l_resolved_data);
|
|
283
|
+
|
|
284
|
+
ELSE
|
|
285
|
+
RAISE WARNING 'Unknown graph rule action type: %', l_action_type;
|
|
286
|
+
END CASE;
|
|
287
|
+
|
|
288
|
+
-- Log successful execution
|
|
289
|
+
l_execution_log := l_execution_log || jsonb_build_object(
|
|
290
|
+
'rule', l_rule_name,
|
|
291
|
+
'action', l_action_type,
|
|
292
|
+
'status', 'success'
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
EXCEPTION WHEN OTHERS THEN
|
|
296
|
+
-- Log error and re-raise for validate actions, otherwise just log
|
|
297
|
+
l_execution_log := l_execution_log || jsonb_build_object(
|
|
298
|
+
'rule', l_rule_name,
|
|
299
|
+
'action', l_action_type,
|
|
300
|
+
'status', 'error',
|
|
301
|
+
'error', SQLERRM
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
-- Re-raise exceptions from validate actions to prevent operation
|
|
305
|
+
IF l_action_type = 'validate' THEN
|
|
306
|
+
RAISE;
|
|
307
|
+
END IF;
|
|
308
|
+
END;
|
|
309
|
+
END LOOP;
|
|
310
|
+
END IF;
|
|
311
|
+
END LOOP;
|
|
312
|
+
|
|
313
|
+
RETURN jsonb_build_object(
|
|
314
|
+
'status', 'success',
|
|
315
|
+
'trigger', l_trigger_key,
|
|
316
|
+
'executed_actions', l_execution_log
|
|
317
|
+
);
|
|
318
|
+
END $$;
|
|
319
|
+
|
|
320
|
+
-- === Update validate_graph_rules to accept new action types ===
|
|
321
|
+
CREATE OR REPLACE FUNCTION dzql.validate_graph_rules(
|
|
322
|
+
p_rules jsonb
|
|
323
|
+
) RETURNS boolean
|
|
324
|
+
LANGUAGE plpgsql AS $$
|
|
325
|
+
DECLARE
|
|
326
|
+
l_trigger_key text;
|
|
327
|
+
l_trigger_rules jsonb;
|
|
328
|
+
l_rule_name text;
|
|
329
|
+
l_rule_config jsonb;
|
|
330
|
+
l_action jsonb;
|
|
331
|
+
l_action_type text;
|
|
332
|
+
BEGIN
|
|
333
|
+
-- Check if rules is empty or null (valid)
|
|
334
|
+
IF p_rules IS NULL OR p_rules = '{}' THEN
|
|
335
|
+
RETURN true;
|
|
336
|
+
END IF;
|
|
337
|
+
|
|
338
|
+
-- Validate top-level trigger types
|
|
339
|
+
FOR l_trigger_key, l_trigger_rules IN SELECT * FROM jsonb_each(p_rules)
|
|
340
|
+
LOOP
|
|
341
|
+
-- Check valid trigger types
|
|
342
|
+
IF l_trigger_key NOT IN ('on_create', 'on_update', 'on_delete', 'on_field_change') THEN
|
|
343
|
+
RAISE WARNING 'Invalid trigger type: %', l_trigger_key;
|
|
344
|
+
RETURN false;
|
|
345
|
+
END IF;
|
|
346
|
+
|
|
347
|
+
-- Validate each rule within the trigger
|
|
348
|
+
FOR l_rule_name, l_rule_config IN SELECT * FROM jsonb_each(l_trigger_rules)
|
|
349
|
+
LOOP
|
|
350
|
+
-- Check required fields
|
|
351
|
+
IF NOT l_rule_config ? 'actions' THEN
|
|
352
|
+
RAISE WARNING 'Rule % missing required "actions" field', l_rule_name;
|
|
353
|
+
RETURN false;
|
|
354
|
+
END IF;
|
|
355
|
+
|
|
356
|
+
-- Validate each action
|
|
357
|
+
FOR l_action IN SELECT * FROM jsonb_array_elements(l_rule_config->'actions')
|
|
358
|
+
LOOP
|
|
359
|
+
l_action_type := l_action->>'type';
|
|
360
|
+
|
|
361
|
+
-- Check valid action types (includes new 'validate' and 'execute' types)
|
|
362
|
+
IF l_action_type NOT IN ('create', 'update', 'delete', 'validate', 'execute') THEN
|
|
363
|
+
RAISE WARNING 'Invalid action type: %', l_action_type;
|
|
364
|
+
RETURN false;
|
|
365
|
+
END IF;
|
|
366
|
+
|
|
367
|
+
-- Check required fields per action type
|
|
368
|
+
IF l_action_type IN ('create', 'update') AND NOT l_action ? 'entity' THEN
|
|
369
|
+
RAISE WARNING 'Action type % requires "entity" field', l_action_type;
|
|
370
|
+
RETURN false;
|
|
371
|
+
END IF;
|
|
372
|
+
|
|
373
|
+
IF l_action_type = 'create' AND NOT l_action ? 'data' THEN
|
|
374
|
+
RAISE WARNING 'Action type "create" requires "data" field';
|
|
375
|
+
RETURN false;
|
|
376
|
+
END IF;
|
|
377
|
+
|
|
378
|
+
IF l_action_type IN ('update', 'delete') AND NOT l_action ? 'match' THEN
|
|
379
|
+
RAISE WARNING 'Action type % requires "match" field', l_action_type;
|
|
380
|
+
RETURN false;
|
|
381
|
+
END IF;
|
|
382
|
+
|
|
383
|
+
-- Validate new action types
|
|
384
|
+
IF l_action_type IN ('validate', 'execute') AND NOT l_action ? 'function' THEN
|
|
385
|
+
RAISE WARNING 'Action type % requires "function" field', l_action_type;
|
|
386
|
+
RETURN false;
|
|
387
|
+
END IF;
|
|
388
|
+
|
|
389
|
+
IF l_action_type IN ('validate', 'execute') AND NOT l_action ? 'params' THEN
|
|
390
|
+
RAISE WARNING 'Action type % requires "params" field', l_action_type;
|
|
391
|
+
RETURN false;
|
|
392
|
+
END IF;
|
|
393
|
+
END LOOP;
|
|
394
|
+
END LOOP;
|
|
395
|
+
END LOOP;
|
|
396
|
+
|
|
397
|
+
RETURN true;
|
|
398
|
+
END $$;
|