dzql 0.1.1 → 0.1.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/GETTING_STARTED.md +1 -1
- package/README.md +1 -1
- package/REFERENCE.md +74 -5
- package/package.json +2 -3
- package/src/database/migrations/002_functions.sql +89 -2
- package/src/database/migrations/003_operations.sql +10 -18
- package/src/database/migrations/004_search.sql +7 -0
- package/src/database/migrations/005_entities.sql +138 -31
- package/CLAUDE.md +0 -931
- package/src/database/migrations/010_graph_rules_validation.sql +0 -398
package/GETTING_STARTED.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
DZQL is a PostgreSQL framework that gives you **atomic real-time updates** via WebSocket. Every database change broadcasts instantly to all connected clients. Zero boilerplate.
|
|
4
4
|
|
|
5
|
-
> **See also:** [REFERENCE.md](REFERENCE.md) for complete API documentation | [CLAUDE.md](../../CLAUDE.md) for AI development guide
|
|
5
|
+
> **See also:** [REFERENCE.md](REFERENCE.md) for complete API documentation | [CLAUDE.md](../../docs/CLAUDE.md) for AI development guide
|
|
6
6
|
|
|
7
7
|
## The Core Pattern
|
|
8
8
|
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ All documentation is maintained in the repository root:
|
|
|
9
9
|
- **[README.md](../../README.md)** - Project overview and quick start
|
|
10
10
|
- **[GETTING_STARTED.md](GETTING_STARTED.md)** - Complete tutorial with working todo app
|
|
11
11
|
- **[REFERENCE.md](REFERENCE.md)** - Complete API reference
|
|
12
|
-
- **[CLAUDE.md](../../CLAUDE.md)** - Development guide for AI assistants
|
|
12
|
+
- **[CLAUDE.md](../../docs/CLAUDE.md)** - Development guide for AI assistants
|
|
13
13
|
- **[Venues Example](../venues/)** - Full working application
|
|
14
14
|
|
|
15
15
|
## Quick Install
|
package/REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DZQL API Reference
|
|
2
2
|
|
|
3
|
-
Complete API documentation for DZQL framework. For tutorials, see [GETTING_STARTED.md](GETTING_STARTED.md). For AI development guide, see [CLAUDE.md](../../CLAUDE.md).
|
|
3
|
+
Complete API documentation for DZQL framework. For tutorials, see [GETTING_STARTED.md](GETTING_STARTED.md). For AI development guide, see [CLAUDE.md](../../docs/CLAUDE.md).
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
@@ -403,12 +403,16 @@ Automatically manage entity relationships when data changes.
|
|
|
403
403
|
"on_create": {
|
|
404
404
|
"rule_name": {
|
|
405
405
|
"description": "Human-readable description",
|
|
406
|
+
"condition": "@after.field = 'value'", // Optional: only run if condition is true
|
|
406
407
|
"actions": [
|
|
407
408
|
{
|
|
408
|
-
"type": "create|update|delete",
|
|
409
|
-
"entity": "target_table",
|
|
409
|
+
"type": "create|update|delete|validate|execute",
|
|
410
|
+
"entity": "target_table", // for create/update/delete
|
|
410
411
|
"data": {"field": "@variable"}, // for create/update
|
|
411
|
-
"match": {"field": "@variable"}
|
|
412
|
+
"match": {"field": "@variable"}, // for update/delete
|
|
413
|
+
"function": "function_name", // for validate/execute
|
|
414
|
+
"params": {"param": "@variable"}, // for validate/execute
|
|
415
|
+
"error_message": "Validation failed" // for validate (optional)
|
|
412
416
|
}
|
|
413
417
|
]
|
|
414
418
|
}
|
|
@@ -425,6 +429,8 @@ Automatically manage entity relationships when data changes.
|
|
|
425
429
|
| `create` | `entity`, `data` | INSERT new record |
|
|
426
430
|
| `update` | `entity`, `match`, `data` | UPDATE matching records |
|
|
427
431
|
| `delete` | `entity`, `match` | DELETE matching records |
|
|
432
|
+
| `validate` | `function`, `params`, `error_message` | Call validation function, rollback if returns false |
|
|
433
|
+
| `execute` | `function`, `params` | Fire-and-forget function execution |
|
|
428
434
|
|
|
429
435
|
### Variables
|
|
430
436
|
|
|
@@ -499,6 +505,69 @@ Variables reference data from the triggering operation:
|
|
|
499
505
|
}
|
|
500
506
|
```
|
|
501
507
|
|
|
508
|
+
#### Data Validation
|
|
509
|
+
```jsonb
|
|
510
|
+
{
|
|
511
|
+
"on_create": {
|
|
512
|
+
"validate_positive_price": {
|
|
513
|
+
"description": "Ensure price is positive",
|
|
514
|
+
"actions": [{
|
|
515
|
+
"type": "validate",
|
|
516
|
+
"function": "validate_positive_value",
|
|
517
|
+
"params": {"p_value": "@price"},
|
|
518
|
+
"error_message": "Price must be positive"
|
|
519
|
+
}]
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Note:** Validation function must return BOOLEAN:
|
|
526
|
+
```sql
|
|
527
|
+
CREATE FUNCTION validate_positive_value(p_value INT)
|
|
528
|
+
RETURNS BOOLEAN AS $$
|
|
529
|
+
SELECT p_value > 0;
|
|
530
|
+
$$ LANGUAGE sql;
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
#### Conditional Execution
|
|
534
|
+
```jsonb
|
|
535
|
+
{
|
|
536
|
+
"on_update": {
|
|
537
|
+
"prevent_posted_changes": {
|
|
538
|
+
"description": "Prevent modification of posted records",
|
|
539
|
+
"condition": "@before.status = 'posted'",
|
|
540
|
+
"actions": [{
|
|
541
|
+
"type": "validate",
|
|
542
|
+
"function": "always_false",
|
|
543
|
+
"params": {},
|
|
544
|
+
"error_message": "Cannot modify posted records"
|
|
545
|
+
}]
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Available in conditions:** `@before.field`, `@after.field`, `@user_id`, and SQL expressions.
|
|
552
|
+
|
|
553
|
+
#### Fire-and-Forget Actions
|
|
554
|
+
```jsonb
|
|
555
|
+
{
|
|
556
|
+
"on_create": {
|
|
557
|
+
"send_notification": {
|
|
558
|
+
"description": "Notify external system",
|
|
559
|
+
"actions": [{
|
|
560
|
+
"type": "execute",
|
|
561
|
+
"function": "log_event",
|
|
562
|
+
"params": {"p_event": "New record created", "p_record_id": "@id"}
|
|
563
|
+
}]
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**Note:** Execute actions don't affect transaction. Function errors are logged but don't rollback.
|
|
570
|
+
|
|
502
571
|
### Execution
|
|
503
572
|
|
|
504
573
|
- **Atomic**: All rules execute in the same transaction
|
|
@@ -886,6 +955,6 @@ const result = await db.api.myCustomFunction({param: 'value'}, userId);
|
|
|
886
955
|
## See Also
|
|
887
956
|
|
|
888
957
|
- [GETTING_STARTED.md](GETTING_STARTED.md) - Hands-on tutorial
|
|
889
|
-
- [CLAUDE.md](../../CLAUDE.md) - AI development guide
|
|
958
|
+
- [CLAUDE.md](../../docs/CLAUDE.md) - AI development guide
|
|
890
959
|
- [README.md](../../README.md) - Project overview
|
|
891
960
|
- [Venues Example](../venues/) - Complete working application
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dzql",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|
|
@@ -16,12 +16,11 @@
|
|
|
16
16
|
"README.md",
|
|
17
17
|
"GETTING_STARTED.md",
|
|
18
18
|
"REFERENCE.md",
|
|
19
|
-
"CLAUDE.md",
|
|
20
19
|
"LICENSE"
|
|
21
20
|
],
|
|
22
21
|
"scripts": {
|
|
23
22
|
"test": "bun test",
|
|
24
|
-
"prepublishOnly": "echo '
|
|
23
|
+
"prepublishOnly": "echo '✅ Publishing DZQL v0.1.3...'"
|
|
25
24
|
},
|
|
26
25
|
"dependencies": {
|
|
27
26
|
"jose": "^6.1.0",
|
|
@@ -149,6 +149,82 @@ BEGIN
|
|
|
149
149
|
RETURN l_result;
|
|
150
150
|
END $$;
|
|
151
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
|
+
|
|
152
228
|
-- === Path Resolution Functions ===
|
|
153
229
|
-- Resolve notification/permission paths to user IDs
|
|
154
230
|
-- === Path Resolution Functions ===
|
|
@@ -651,8 +727,8 @@ BEGIN
|
|
|
651
727
|
LOOP
|
|
652
728
|
l_action_type := l_action->>'type';
|
|
653
729
|
|
|
654
|
-
-- Check valid action types
|
|
655
|
-
IF l_action_type NOT IN ('create', 'update', 'delete') THEN
|
|
730
|
+
-- Check valid action types (includes new 'validate' and 'execute' types)
|
|
731
|
+
IF l_action_type NOT IN ('create', 'update', 'delete', 'validate', 'execute') THEN
|
|
656
732
|
RAISE WARNING 'Invalid action type: %', l_action_type;
|
|
657
733
|
RETURN false;
|
|
658
734
|
END IF;
|
|
@@ -672,6 +748,17 @@ BEGIN
|
|
|
672
748
|
RAISE WARNING 'Action type % requires "match" field', l_action_type;
|
|
673
749
|
RETURN false;
|
|
674
750
|
END IF;
|
|
751
|
+
|
|
752
|
+
-- Validate new action types
|
|
753
|
+
IF l_action_type IN ('validate', 'execute') AND NOT l_action ? 'function' THEN
|
|
754
|
+
RAISE WARNING 'Action type % requires "function" field', l_action_type;
|
|
755
|
+
RETURN false;
|
|
756
|
+
END IF;
|
|
757
|
+
|
|
758
|
+
IF l_action_type IN ('validate', 'execute') AND NOT l_action ? 'params' THEN
|
|
759
|
+
RAISE WARNING 'Action type % requires "params" field', l_action_type;
|
|
760
|
+
RETURN false;
|
|
761
|
+
END IF;
|
|
675
762
|
END LOOP;
|
|
676
763
|
END LOOP;
|
|
677
764
|
END LOOP;
|
|
@@ -301,8 +301,16 @@ BEGIN
|
|
|
301
301
|
EXECUTE l_sql_stmt INTO l_existing_record;
|
|
302
302
|
|
|
303
303
|
IF l_existing_record IS NULL THEN
|
|
304
|
-
--
|
|
305
|
-
|
|
304
|
+
-- Record doesn't exist. For composite keys, treat as INSERT.
|
|
305
|
+
-- For single-column PKs, this is an error (user provided non-existent ID).
|
|
306
|
+
IF array_length(l_pk_cols, 1) > 1 THEN
|
|
307
|
+
-- Composite key: treat as INSERT
|
|
308
|
+
l_is_insert := true;
|
|
309
|
+
ELSE
|
|
310
|
+
-- Single PK: this is an error
|
|
311
|
+
RAISE EXCEPTION 'DZQL: record with %=% not found in %',
|
|
312
|
+
l_pk_cols[1], l_args_json ->> l_pk_cols[1], p_entity;
|
|
313
|
+
END IF;
|
|
306
314
|
END IF;
|
|
307
315
|
END IF;
|
|
308
316
|
|
|
@@ -337,14 +345,6 @@ BEGIN
|
|
|
337
345
|
p_entity);
|
|
338
346
|
EXECUTE l_sql_stmt INTO l_result;
|
|
339
347
|
|
|
340
|
-
-- Execute graph rules for update
|
|
341
|
-
l_graph_rules_result := dzql.execute_graph_rules(
|
|
342
|
-
p_entity,
|
|
343
|
-
'update',
|
|
344
|
-
l_existing_record,
|
|
345
|
-
l_result,
|
|
346
|
-
p_user_id
|
|
347
|
-
);
|
|
348
348
|
|
|
349
349
|
ELSE
|
|
350
350
|
-- INSERT: Use provided values, let database handle defaults
|
|
@@ -379,14 +379,6 @@ BEGIN
|
|
|
379
379
|
p_entity);
|
|
380
380
|
EXECUTE l_sql_stmt INTO l_result;
|
|
381
381
|
|
|
382
|
-
-- Execute graph rules for insert
|
|
383
|
-
l_graph_rules_result := dzql.execute_graph_rules(
|
|
384
|
-
p_entity,
|
|
385
|
-
'insert',
|
|
386
|
-
NULL,
|
|
387
|
-
l_result,
|
|
388
|
-
p_user_id
|
|
389
|
-
);
|
|
390
382
|
END IF;
|
|
391
383
|
|
|
392
384
|
-- Execute graph rules for the appropriate operation
|
|
@@ -342,6 +342,13 @@ BEGIN
|
|
|
342
342
|
END IF;
|
|
343
343
|
END IF;
|
|
344
344
|
|
|
345
|
+
-- Add permission check to WHERE clause
|
|
346
|
+
IF l_where_clause = '' OR l_where_clause = 'WHERE' THEN
|
|
347
|
+
l_where_clause := format('WHERE dzql.check_permission(%L, ''view'', %L, to_jsonb(t.*))', p_user_id, p_entity);
|
|
348
|
+
ELSE
|
|
349
|
+
l_where_clause := l_where_clause || format(' AND dzql.check_permission(%L, ''view'', %L, to_jsonb(t.*))', p_user_id, p_entity);
|
|
350
|
+
END IF;
|
|
351
|
+
|
|
345
352
|
-- Build base SQL
|
|
346
353
|
l_base_sql := format('FROM %I t %s', p_entity, l_where_clause);
|
|
347
354
|
|
|
@@ -163,6 +163,90 @@ BEGIN
|
|
|
163
163
|
);
|
|
164
164
|
END $$;
|
|
165
165
|
|
|
166
|
+
-- === Validate Action ===
|
|
167
|
+
-- Calls a validation function and raises exception if it returns false
|
|
168
|
+
CREATE OR REPLACE FUNCTION dzql.execute_graph_validate(
|
|
169
|
+
p_function_name text,
|
|
170
|
+
p_params jsonb,
|
|
171
|
+
p_error_message text DEFAULT 'Validation failed'
|
|
172
|
+
) RETURNS void
|
|
173
|
+
LANGUAGE plpgsql AS $$
|
|
174
|
+
DECLARE
|
|
175
|
+
l_result boolean;
|
|
176
|
+
l_sql text;
|
|
177
|
+
l_param_list text[];
|
|
178
|
+
l_key text;
|
|
179
|
+
l_value text;
|
|
180
|
+
BEGIN
|
|
181
|
+
-- Validate function name (prevent SQL injection)
|
|
182
|
+
IF NOT p_function_name ~ '^[a-z_][a-z0-9_]*$' THEN
|
|
183
|
+
RAISE EXCEPTION 'Invalid function name: %', p_function_name;
|
|
184
|
+
END IF;
|
|
185
|
+
|
|
186
|
+
-- Build parameter list
|
|
187
|
+
l_param_list := array[]::text[];
|
|
188
|
+
FOR l_key, l_value IN SELECT * FROM jsonb_each_text(p_params)
|
|
189
|
+
LOOP
|
|
190
|
+
l_param_list := l_param_list || (l_key || ' => ' || quote_literal(l_value));
|
|
191
|
+
END LOOP;
|
|
192
|
+
|
|
193
|
+
-- Build and execute function call
|
|
194
|
+
IF array_length(l_param_list, 1) > 0 THEN
|
|
195
|
+
l_sql := format('SELECT %I(%s)', p_function_name, array_to_string(l_param_list, ', '));
|
|
196
|
+
ELSE
|
|
197
|
+
l_sql := format('SELECT %I()', p_function_name);
|
|
198
|
+
END IF;
|
|
199
|
+
|
|
200
|
+
EXECUTE l_sql INTO l_result;
|
|
201
|
+
|
|
202
|
+
-- Raise exception if validation failed
|
|
203
|
+
IF NOT COALESCE(l_result, false) THEN
|
|
204
|
+
RAISE EXCEPTION '%', p_error_message;
|
|
205
|
+
END IF;
|
|
206
|
+
END $$;
|
|
207
|
+
|
|
208
|
+
-- === Execute Action ===
|
|
209
|
+
-- Calls a custom function with parameters (fire-and-forget)
|
|
210
|
+
CREATE OR REPLACE FUNCTION dzql.execute_graph_function(
|
|
211
|
+
p_function_name text,
|
|
212
|
+
p_params jsonb
|
|
213
|
+
) RETURNS jsonb
|
|
214
|
+
LANGUAGE plpgsql AS $$
|
|
215
|
+
DECLARE
|
|
216
|
+
l_result jsonb;
|
|
217
|
+
l_sql text;
|
|
218
|
+
l_param_list text[];
|
|
219
|
+
l_key text;
|
|
220
|
+
l_value text;
|
|
221
|
+
BEGIN
|
|
222
|
+
-- Validate function name (prevent SQL injection)
|
|
223
|
+
IF NOT p_function_name ~ '^[a-z_][a-z0-9_]*$' THEN
|
|
224
|
+
RAISE EXCEPTION 'Invalid function name: %', p_function_name;
|
|
225
|
+
END IF;
|
|
226
|
+
|
|
227
|
+
-- Build parameter list
|
|
228
|
+
l_param_list := array[]::text[];
|
|
229
|
+
FOR l_key, l_value IN SELECT * FROM jsonb_each_text(p_params)
|
|
230
|
+
LOOP
|
|
231
|
+
l_param_list := l_param_list || (l_key || ' => ' || quote_literal(l_value));
|
|
232
|
+
END LOOP;
|
|
233
|
+
|
|
234
|
+
-- Build and execute function call
|
|
235
|
+
IF array_length(l_param_list, 1) > 0 THEN
|
|
236
|
+
l_sql := format('SELECT %I(%s)', p_function_name, array_to_string(l_param_list, ', '));
|
|
237
|
+
ELSE
|
|
238
|
+
l_sql := format('SELECT %I()', p_function_name);
|
|
239
|
+
END IF;
|
|
240
|
+
|
|
241
|
+
EXECUTE l_sql INTO l_result;
|
|
242
|
+
|
|
243
|
+
RETURN COALESCE(l_result, '{}'::jsonb);
|
|
244
|
+
EXCEPTION WHEN OTHERS THEN
|
|
245
|
+
-- Log error but don't fail the transaction
|
|
246
|
+
RAISE WARNING 'Graph rule function execution failed: % (function: %)', SQLERRM, p_function_name;
|
|
247
|
+
RETURN jsonb_build_object('error', SQLERRM);
|
|
248
|
+
END $$;
|
|
249
|
+
|
|
166
250
|
-- Main graph rules execution engine
|
|
167
251
|
CREATE OR REPLACE FUNCTION dzql.execute_graph_rules(
|
|
168
252
|
p_table_name text,
|
|
@@ -189,6 +273,10 @@ DECLARE
|
|
|
189
273
|
l_execution_log jsonb := '[]'::jsonb;
|
|
190
274
|
l_condition text;
|
|
191
275
|
l_condition_result boolean;
|
|
276
|
+
l_function_name text;
|
|
277
|
+
l_function_params jsonb;
|
|
278
|
+
l_error_message text;
|
|
279
|
+
l_function_result jsonb;
|
|
192
280
|
BEGIN
|
|
193
281
|
-- Get entity configuration
|
|
194
282
|
SELECT * INTO l_entity_config FROM dzql.entities WHERE table_name = p_table_name;
|
|
@@ -230,69 +318,88 @@ BEGIN
|
|
|
230
318
|
l_condition := l_rule_config->>'condition';
|
|
231
319
|
l_condition_result := true; -- Default to true if no condition
|
|
232
320
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
-- END IF;
|
|
321
|
+
IF l_condition IS NOT NULL THEN
|
|
322
|
+
l_condition_result := dzql.evaluate_condition(l_condition, p_record_before, p_record_after, p_user_id);
|
|
323
|
+
END IF;
|
|
237
324
|
|
|
238
325
|
IF l_condition_result THEN
|
|
239
326
|
-- Execute each action in the rule
|
|
240
327
|
FOR l_action IN SELECT * FROM jsonb_array_elements(l_rule_config->'actions')
|
|
241
328
|
LOOP
|
|
242
329
|
l_action_type := l_action->>'type';
|
|
243
|
-
l_target_entity := l_action->>'entity';
|
|
244
|
-
l_action_data := l_action->'data';
|
|
245
|
-
l_action_match := l_action->'match';
|
|
246
|
-
|
|
247
|
-
-- Resolve variables in data and match objects
|
|
248
|
-
IF l_action_data IS NOT NULL THEN
|
|
249
|
-
l_resolved_data := dzql.resolve_graph_data(l_action_data, p_record_before, p_record_after, p_user_id);
|
|
250
|
-
END IF;
|
|
251
330
|
|
|
252
|
-
|
|
253
|
-
l_resolved_match := dzql.resolve_graph_data(l_action_match, p_record_before, p_record_after, p_user_id);
|
|
254
|
-
END IF;
|
|
255
|
-
|
|
256
|
-
-- Execute the action
|
|
331
|
+
-- Execute the action based on type
|
|
257
332
|
BEGIN
|
|
258
333
|
CASE l_action_type
|
|
334
|
+
-- Existing action types
|
|
259
335
|
WHEN 'create' THEN
|
|
336
|
+
l_target_entity := l_action->>'entity';
|
|
337
|
+
l_action_data := l_action->'data';
|
|
338
|
+
l_resolved_data := dzql.resolve_graph_data(l_action_data, p_record_before, p_record_after, p_user_id);
|
|
260
339
|
PERFORM dzql.execute_graph_insert(l_target_entity, l_resolved_data, p_user_id);
|
|
261
340
|
|
|
262
341
|
WHEN 'update' THEN
|
|
342
|
+
l_target_entity := l_action->>'entity';
|
|
343
|
+
l_action_data := l_action->'data';
|
|
344
|
+
l_action_match := l_action->'match';
|
|
345
|
+
l_resolved_data := dzql.resolve_graph_data(l_action_data, p_record_before, p_record_after, p_user_id);
|
|
346
|
+
l_resolved_match := dzql.resolve_graph_data(l_action_match, p_record_before, p_record_after, p_user_id);
|
|
263
347
|
PERFORM dzql.execute_graph_update(l_target_entity, l_resolved_match, l_resolved_data, p_user_id);
|
|
264
348
|
|
|
265
349
|
WHEN 'delete' THEN
|
|
350
|
+
l_target_entity := l_action->>'entity';
|
|
351
|
+
l_action_match := l_action->'match';
|
|
352
|
+
l_resolved_match := dzql.resolve_graph_data(l_action_match, p_record_before, p_record_after, p_user_id);
|
|
266
353
|
PERFORM dzql.execute_graph_delete(l_target_entity, l_resolved_match, p_user_id);
|
|
354
|
+
|
|
355
|
+
-- NEW: Validation action
|
|
356
|
+
WHEN 'validate' THEN
|
|
357
|
+
l_function_name := l_action->>'function';
|
|
358
|
+
l_function_params := l_action->'params';
|
|
359
|
+
l_error_message := COALESCE(l_action->>'error_message', 'Validation failed');
|
|
360
|
+
l_resolved_data := dzql.resolve_graph_data(l_function_params, p_record_before, p_record_after, p_user_id);
|
|
361
|
+
PERFORM dzql.execute_graph_validate(l_function_name, l_resolved_data, l_error_message);
|
|
362
|
+
|
|
363
|
+
-- NEW: Execute function action
|
|
364
|
+
WHEN 'execute' THEN
|
|
365
|
+
l_function_name := l_action->>'function';
|
|
366
|
+
l_function_params := l_action->'params';
|
|
367
|
+
l_resolved_data := dzql.resolve_graph_data(l_function_params, p_record_before, p_record_after, p_user_id);
|
|
368
|
+
l_function_result := dzql.execute_graph_function(l_function_name, l_resolved_data);
|
|
369
|
+
|
|
370
|
+
ELSE
|
|
371
|
+
RAISE WARNING 'Unknown graph rule action type: %', l_action_type;
|
|
267
372
|
END CASE;
|
|
268
373
|
|
|
269
374
|
-- Log successful execution
|
|
270
375
|
l_execution_log := l_execution_log || jsonb_build_object(
|
|
271
376
|
'rule', l_rule_name,
|
|
272
|
-
'
|
|
273
|
-
'target_entity', l_target_entity,
|
|
377
|
+
'action', l_action_type,
|
|
274
378
|
'status', 'success'
|
|
275
379
|
);
|
|
276
380
|
|
|
277
|
-
EXCEPTION
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
381
|
+
EXCEPTION WHEN OTHERS THEN
|
|
382
|
+
-- Log error and re-raise for validate actions, otherwise just log
|
|
383
|
+
l_execution_log := l_execution_log || jsonb_build_object(
|
|
384
|
+
'rule', l_rule_name,
|
|
385
|
+
'action', l_action_type,
|
|
386
|
+
'status', 'error',
|
|
387
|
+
'error', SQLERRM
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
-- Re-raise exceptions from validate actions to prevent operation
|
|
391
|
+
IF l_action_type = 'validate' THEN
|
|
392
|
+
RAISE;
|
|
393
|
+
END IF;
|
|
287
394
|
END;
|
|
288
395
|
END LOOP;
|
|
289
396
|
END IF;
|
|
290
397
|
END LOOP;
|
|
291
398
|
|
|
292
399
|
RETURN jsonb_build_object(
|
|
293
|
-
'status', '
|
|
400
|
+
'status', 'success',
|
|
294
401
|
'trigger', l_trigger_key,
|
|
295
|
-
'
|
|
402
|
+
'executed_actions', l_execution_log
|
|
296
403
|
);
|
|
297
404
|
END $$;
|
|
298
405
|
|