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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "PostgreSQL-powered framework with zero boilerplate CRUD operations and real-time WebSocket synchronization",
5
5
  "type": "module",
6
6
  "main": "src/server/index.js",
@@ -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 $$;