dzql 0.1.0-alpha.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.
@@ -0,0 +1,511 @@
1
+ -- DZQL Entity Management - Version 3.0.0
2
+ -- Entity registration, API functions creation, and graph rules execution
3
+
4
+ -- ============================================================================
5
+ -- GRAPH RULES EXECUTION ENGINE
6
+ -- ============================================================================
7
+
8
+ -- Execute graph insert action
9
+ CREATE OR REPLACE FUNCTION dzql.execute_graph_insert(
10
+ p_entity text,
11
+ p_data jsonb,
12
+ p_user_id int
13
+ ) RETURNS void
14
+ LANGUAGE plpgsql AS $$
15
+ DECLARE
16
+ l_cols text[];
17
+ l_vals text[];
18
+ l_col_name text;
19
+ l_sql_stmt text;
20
+ BEGIN
21
+ -- Graph rules are trusted server-side operations, skip permission checks
22
+
23
+ -- Build column and value lists
24
+ FOR l_col_name IN SELECT * FROM jsonb_object_keys(p_data)
25
+ LOOP
26
+ l_cols := l_cols || l_col_name;
27
+ l_vals := l_vals || quote_literal(p_data->>l_col_name);
28
+ END LOOP;
29
+
30
+ -- Build and execute INSERT statement
31
+ l_sql_stmt := format('INSERT INTO %I (%s) VALUES (%s)',
32
+ p_entity,
33
+ array_to_string(l_cols, ', '),
34
+ array_to_string(l_vals, ', ')
35
+ );
36
+
37
+ EXECUTE l_sql_stmt;
38
+
39
+ -- Create event for graph rule action
40
+ INSERT INTO dzql.events (
41
+ table_name,
42
+ op,
43
+ pk,
44
+ before,
45
+ after,
46
+ user_id,
47
+ notify_users
48
+ ) VALUES (
49
+ p_entity,
50
+ 'insert',
51
+ jsonb_build_object('id', p_data->>'id'),
52
+ NULL,
53
+ p_data,
54
+ p_user_id,
55
+ dzql.resolve_notification_paths(p_entity, p_data)
56
+ );
57
+ END $$;
58
+
59
+ -- Execute graph update action
60
+ CREATE OR REPLACE FUNCTION dzql.execute_graph_update(
61
+ p_entity text,
62
+ p_match jsonb,
63
+ p_data jsonb,
64
+ p_user_id int
65
+ ) RETURNS void
66
+ LANGUAGE plpgsql AS $$
67
+ DECLARE
68
+ l_set_clauses text[];
69
+ l_where_clauses text[];
70
+ l_col_name text;
71
+ l_sql_stmt text;
72
+ BEGIN
73
+ -- Check permissions before executing graph rule action
74
+ -- Graph rules are trusted server-side operations, skip permission checks
75
+
76
+ -- Build SET clauses
77
+ FOR l_col_name IN SELECT * FROM jsonb_object_keys(p_data)
78
+ LOOP
79
+ l_set_clauses := l_set_clauses || format('%I = %L', l_col_name, p_data->>l_col_name);
80
+ END LOOP;
81
+
82
+ -- Build WHERE clauses
83
+ FOR l_col_name IN SELECT * FROM jsonb_object_keys(p_match)
84
+ LOOP
85
+ l_where_clauses := l_where_clauses || format('%I = %L', l_col_name, p_match->>l_col_name);
86
+ END LOOP;
87
+
88
+ -- Build and execute UPDATE statement
89
+ l_sql_stmt := format('UPDATE %I SET %s WHERE %s',
90
+ p_entity,
91
+ array_to_string(l_set_clauses, ', '),
92
+ array_to_string(l_where_clauses, ' AND ')
93
+ );
94
+
95
+ EXECUTE l_sql_stmt;
96
+
97
+ -- Create event for graph rule action
98
+ -- Note: We don't have the before/after data here, just logging the update occurred
99
+ INSERT INTO dzql.events (
100
+ table_name,
101
+ op,
102
+ pk,
103
+ before,
104
+ after,
105
+ user_id,
106
+ notify_users
107
+ ) VALUES (
108
+ p_entity,
109
+ 'update',
110
+ p_match,
111
+ NULL, -- We don't have the before state in this context
112
+ p_data,
113
+ p_user_id,
114
+ '[]'::int[] -- Graph rule updates don't have notification paths
115
+ );
116
+ END $$;
117
+
118
+ -- Execute graph delete action
119
+ CREATE OR REPLACE FUNCTION dzql.execute_graph_delete(
120
+ p_entity text,
121
+ p_match jsonb,
122
+ p_user_id int
123
+ ) RETURNS void
124
+ LANGUAGE plpgsql AS $$
125
+ DECLARE
126
+ l_where_clauses text[];
127
+ l_col_name text;
128
+ l_sql_stmt text;
129
+ BEGIN
130
+ -- Check permissions before executing graph rule action
131
+ -- Graph rules are trusted server-side operations, skip permission checks
132
+ -- Build WHERE clauses
133
+ FOR l_col_name IN SELECT * FROM jsonb_object_keys(p_match)
134
+ LOOP
135
+ l_where_clauses := l_where_clauses || format('%I = %L', l_col_name, p_match->>l_col_name);
136
+ END LOOP;
137
+
138
+ -- Build and execute DELETE statement
139
+ l_sql_stmt := format('DELETE FROM %I WHERE %s',
140
+ p_entity,
141
+ array_to_string(l_where_clauses, ' AND ')
142
+ );
143
+
144
+ EXECUTE l_sql_stmt;
145
+
146
+ -- Create event for graph rule action
147
+ INSERT INTO dzql.events (
148
+ table_name,
149
+ op,
150
+ pk,
151
+ before,
152
+ after,
153
+ user_id,
154
+ notify_users
155
+ ) VALUES (
156
+ p_entity,
157
+ 'delete',
158
+ p_match,
159
+ NULL, -- We don't have the before state in this context
160
+ NULL,
161
+ p_user_id,
162
+ '[]'::int[] -- Graph rule deletes don't have notification paths
163
+ );
164
+ END $$;
165
+
166
+ -- Main graph rules execution engine
167
+ CREATE OR REPLACE FUNCTION dzql.execute_graph_rules(
168
+ p_table_name text,
169
+ p_operation text, -- 'insert', 'update', 'delete'
170
+ p_record_before jsonb,
171
+ p_record_after jsonb,
172
+ p_user_id int
173
+ ) RETURNS jsonb
174
+ LANGUAGE plpgsql AS $$
175
+ DECLARE
176
+ l_entity_config record;
177
+ l_graph_rules jsonb;
178
+ l_trigger_key text;
179
+ l_trigger_rules jsonb;
180
+ l_rule_name text;
181
+ l_rule_config jsonb;
182
+ l_action jsonb;
183
+ l_action_type text;
184
+ l_target_entity text;
185
+ l_action_data jsonb;
186
+ l_action_match jsonb;
187
+ l_resolved_data jsonb;
188
+ l_resolved_match jsonb;
189
+ l_execution_log jsonb := '[]'::jsonb;
190
+ l_condition text;
191
+ l_condition_result boolean;
192
+ BEGIN
193
+ -- Get entity configuration
194
+ SELECT * INTO l_entity_config FROM dzql.entities WHERE table_name = p_table_name;
195
+
196
+ IF l_entity_config IS NULL THEN
197
+ RETURN jsonb_build_object('status', 'entity_not_found');
198
+ END IF;
199
+
200
+ l_graph_rules := l_entity_config.graph_rules;
201
+
202
+ -- Early exit if no graph rules
203
+ IF l_graph_rules IS NULL OR l_graph_rules = '{}' THEN
204
+ RETURN jsonb_build_object('status', 'no_rules');
205
+ END IF;
206
+
207
+ -- Map operation to trigger key
208
+ l_trigger_key := CASE p_operation
209
+ WHEN 'insert' THEN 'on_create'
210
+ WHEN 'update' THEN 'on_update'
211
+ WHEN 'delete' THEN 'on_delete'
212
+ ELSE NULL
213
+ END;
214
+
215
+ IF l_trigger_key IS NULL THEN
216
+ RETURN jsonb_build_object('status', 'invalid_operation', 'operation', p_operation);
217
+ END IF;
218
+
219
+ -- Get rules for this trigger
220
+ l_trigger_rules := l_graph_rules->l_trigger_key;
221
+
222
+ IF l_trigger_rules IS NULL OR l_trigger_rules = '{}' THEN
223
+ RETURN jsonb_build_object('status', 'no_rules_for_trigger', 'trigger', l_trigger_key);
224
+ END IF;
225
+
226
+ -- Execute each rule
227
+ FOR l_rule_name, l_rule_config IN SELECT * FROM jsonb_each(l_trigger_rules)
228
+ LOOP
229
+ -- Check condition if present
230
+ l_condition := l_rule_config->>'condition';
231
+ l_condition_result := true; -- Default to true if no condition
232
+
233
+ -- TODO: Implement condition evaluation
234
+ -- IF l_condition IS NOT NULL THEN
235
+ -- l_condition_result := dzql.evaluate_condition(l_condition, p_record_before, p_record_after, p_user_id);
236
+ -- END IF;
237
+
238
+ IF l_condition_result THEN
239
+ -- Execute each action in the rule
240
+ FOR l_action IN SELECT * FROM jsonb_array_elements(l_rule_config->'actions')
241
+ LOOP
242
+ 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
+
252
+ IF l_action_match IS NOT NULL THEN
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
257
+ BEGIN
258
+ CASE l_action_type
259
+ WHEN 'create' THEN
260
+ PERFORM dzql.execute_graph_insert(l_target_entity, l_resolved_data, p_user_id);
261
+
262
+ WHEN 'update' THEN
263
+ PERFORM dzql.execute_graph_update(l_target_entity, l_resolved_match, l_resolved_data, p_user_id);
264
+
265
+ WHEN 'delete' THEN
266
+ PERFORM dzql.execute_graph_delete(l_target_entity, l_resolved_match, p_user_id);
267
+ END CASE;
268
+
269
+ -- Log successful execution
270
+ l_execution_log := l_execution_log || jsonb_build_object(
271
+ 'rule', l_rule_name,
272
+ 'action_type', l_action_type,
273
+ 'target_entity', l_target_entity,
274
+ 'status', 'success'
275
+ );
276
+
277
+ EXCEPTION
278
+ WHEN OTHERS THEN
279
+ -- Log failed execution but continue with other rules
280
+ l_execution_log := l_execution_log || jsonb_build_object(
281
+ 'rule', l_rule_name,
282
+ 'action_type', l_action_type,
283
+ 'target_entity', l_target_entity,
284
+ 'status', 'error',
285
+ 'error', SQLERRM
286
+ );
287
+ END;
288
+ END LOOP;
289
+ END IF;
290
+ END LOOP;
291
+
292
+ RETURN jsonb_build_object(
293
+ 'status', 'completed',
294
+ 'trigger', l_trigger_key,
295
+ 'execution_log', l_execution_log
296
+ );
297
+ END $$;
298
+
299
+ -- ============================================================================
300
+ -- API FUNCTION CREATION
301
+ -- ============================================================================
302
+
303
+ -- Create API functions for an entity
304
+ CREATE OR REPLACE FUNCTION dzql.create_entity_functions(p_table_name text)
305
+ RETURNS void
306
+ LANGUAGE plpgsql AS $$
307
+ DECLARE
308
+ l_get_fn_name text;
309
+ l_save_fn_name text;
310
+ l_delete_fn_name text;
311
+ l_lookup_fn_name text;
312
+ l_search_fn_name text;
313
+ BEGIN
314
+ -- Generate function names
315
+ l_get_fn_name := 'get_' || p_table_name;
316
+ l_save_fn_name := 'save_' || p_table_name;
317
+ l_delete_fn_name := 'delete_' || p_table_name;
318
+ l_lookup_fn_name := 'lookup_' || p_table_name;
319
+ l_search_fn_name := 'search_' || p_table_name;
320
+
321
+ -- Create GET function
322
+ EXECUTE format('
323
+ CREATE OR REPLACE FUNCTION dzql.%I(p_args jsonb, p_user_id int)
324
+ RETURNS jsonb
325
+ LANGUAGE sql
326
+ AS $func$
327
+ SELECT dzql.generic_get(%L, p_args, p_user_id);
328
+ $func$;
329
+ ', l_get_fn_name, p_table_name);
330
+
331
+ -- Create SAVE function
332
+ EXECUTE format('
333
+ CREATE OR REPLACE FUNCTION dzql.%I(p_args jsonb, p_user_id int)
334
+ RETURNS jsonb
335
+ LANGUAGE sql
336
+ AS $func$
337
+ SELECT dzql.generic_save(%L, p_args, p_user_id);
338
+ $func$;
339
+ ', l_save_fn_name, p_table_name);
340
+
341
+ -- Create DELETE function
342
+ EXECUTE format('
343
+ CREATE OR REPLACE FUNCTION dzql.%I(p_args jsonb, p_user_id int)
344
+ RETURNS jsonb
345
+ LANGUAGE sql
346
+ AS $func$
347
+ SELECT dzql.generic_delete(%L, p_args, p_user_id);
348
+ $func$;
349
+ ', l_delete_fn_name, p_table_name);
350
+
351
+ -- Create LOOKUP function
352
+ EXECUTE format('
353
+ CREATE OR REPLACE FUNCTION dzql.%I(p_args jsonb, p_user_id int)
354
+ RETURNS jsonb
355
+ LANGUAGE sql
356
+ AS $func$
357
+ SELECT dzql.generic_lookup(%L, p_args, p_user_id);
358
+ $func$;
359
+ ', l_lookup_fn_name, p_table_name);
360
+
361
+ -- Create SEARCH function
362
+ EXECUTE format('
363
+ CREATE OR REPLACE FUNCTION dzql.%I(p_args jsonb, p_user_id int)
364
+ RETURNS jsonb
365
+ LANGUAGE sql
366
+ AS $func$
367
+ SELECT dzql.generic_search(%L, p_args, p_user_id);
368
+ $func$;
369
+ ', l_search_fn_name, p_table_name);
370
+ END $$;
371
+
372
+ -- ============================================================================
373
+ -- ENTITY REGISTRATION
374
+ -- ============================================================================
375
+
376
+ -- Register entity function with full graph rules support
377
+ CREATE OR REPLACE FUNCTION dzql.register_entity(
378
+ p_table_name text,
379
+ p_label_field text,
380
+ p_searchable_fields text[],
381
+ p_fk_includes jsonb DEFAULT '{}',
382
+ p_soft_delete boolean DEFAULT false,
383
+ p_temporal_fields jsonb DEFAULT '{}',
384
+ p_notification_paths jsonb DEFAULT '{}',
385
+ p_permission_paths jsonb DEFAULT '{}',
386
+ p_graph_rules jsonb DEFAULT '{}'
387
+ ) RETURNS void
388
+ LANGUAGE plpgsql AS $$
389
+ BEGIN
390
+ -- Validate permission paths if provided
391
+ IF p_permission_paths IS NOT NULL AND p_permission_paths != '{}' THEN
392
+ IF NOT dzql.validate_permission_paths(p_table_name, p_permission_paths) THEN
393
+ RAISE EXCEPTION 'Invalid permission paths for entity %', p_table_name;
394
+ END IF;
395
+ END IF;
396
+
397
+ -- Validate graph rules if provided
398
+ IF p_graph_rules IS NOT NULL AND p_graph_rules != '{}' THEN
399
+ IF NOT dzql.validate_graph_rules(p_graph_rules) THEN
400
+ RAISE EXCEPTION 'Invalid graph rules for entity %', p_table_name;
401
+ END IF;
402
+ END IF;
403
+
404
+ -- Insert or update entity configuration
405
+ INSERT INTO dzql.entities
406
+ (table_name, label_field, searchable_fields, fk_includes, soft_delete, temporal_fields, notification_paths, permission_paths, graph_rules)
407
+ VALUES
408
+ (p_table_name, p_label_field, p_searchable_fields, p_fk_includes, p_soft_delete, p_temporal_fields, p_notification_paths, p_permission_paths, p_graph_rules)
409
+ ON CONFLICT (table_name) DO UPDATE SET
410
+ label_field = EXCLUDED.label_field,
411
+ searchable_fields = EXCLUDED.searchable_fields,
412
+ fk_includes = EXCLUDED.fk_includes,
413
+ soft_delete = EXCLUDED.soft_delete,
414
+ temporal_fields = EXCLUDED.temporal_fields,
415
+ notification_paths = EXCLUDED.notification_paths,
416
+ permission_paths = EXCLUDED.permission_paths,
417
+ graph_rules = EXCLUDED.graph_rules;
418
+
419
+ -- Create API functions for this entity
420
+ PERFORM dzql.create_entity_functions(p_table_name);
421
+
422
+ -- Log successful registration
423
+ RAISE NOTICE 'DZQL: Entity % registered successfully with graph rules support', p_table_name;
424
+ END $$;
425
+
426
+ -- ============================================================================
427
+ -- ENTITY UTILITIES
428
+ -- ============================================================================
429
+
430
+ -- Unregister an entity (removes configuration and API functions)
431
+ CREATE OR REPLACE FUNCTION dzql.unregister_entity(p_table_name text)
432
+ RETURNS void
433
+ LANGUAGE plpgsql AS $$
434
+ DECLARE
435
+ l_fn_names text[] := ARRAY['get_', 'save_', 'delete_', 'lookup_', 'search_'];
436
+ l_fn_name text;
437
+ BEGIN
438
+ -- Remove entity configuration
439
+ DELETE FROM dzql.entities WHERE table_name = p_table_name;
440
+
441
+ -- Drop API functions
442
+ FOREACH l_fn_name IN ARRAY l_fn_names
443
+ LOOP
444
+ EXECUTE format('DROP FUNCTION IF EXISTS dzql.%I(jsonb, int)', l_fn_name || p_table_name);
445
+ END LOOP;
446
+
447
+ RAISE NOTICE 'DZQL: Entity % unregistered successfully', p_table_name;
448
+ END $$;
449
+
450
+ -- List all registered entities
451
+ CREATE OR REPLACE FUNCTION dzql.list_entities()
452
+ RETURNS TABLE(
453
+ table_name text,
454
+ label_field text,
455
+ searchable_fields text[],
456
+ has_fk_includes boolean,
457
+ soft_delete boolean,
458
+ has_temporal_fields boolean,
459
+ has_notification_paths boolean,
460
+ has_permission_paths boolean,
461
+ has_graph_rules boolean
462
+ )
463
+ LANGUAGE sql AS $$
464
+ SELECT
465
+ e.table_name,
466
+ e.label_field,
467
+ e.searchable_fields,
468
+ (e.fk_includes IS NOT NULL AND e.fk_includes != '{}') as has_fk_includes,
469
+ e.soft_delete,
470
+ (e.temporal_fields IS NOT NULL AND e.temporal_fields != '{}') as has_temporal_fields,
471
+ (e.notification_paths IS NOT NULL AND e.notification_paths != '{}') as has_notification_paths,
472
+ (e.permission_paths IS NOT NULL AND e.permission_paths != '{}') as has_permission_paths,
473
+ (e.graph_rules IS NOT NULL AND e.graph_rules != '{}') as has_graph_rules
474
+ FROM dzql.entities e
475
+ ORDER BY e.table_name;
476
+ $$;
477
+
478
+ -- Get detailed entity configuration
479
+ CREATE OR REPLACE FUNCTION dzql.get_entity_config(p_table_name text)
480
+ RETURNS jsonb
481
+ LANGUAGE sql AS $$
482
+ SELECT to_jsonb(e.*)
483
+ FROM dzql.entities e
484
+ WHERE e.table_name = p_table_name;
485
+ $$;
486
+
487
+ -- Update entity graph rules only
488
+ CREATE OR REPLACE FUNCTION dzql.update_entity_graph_rules(
489
+ p_table_name text,
490
+ p_graph_rules jsonb
491
+ ) RETURNS void
492
+ LANGUAGE plpgsql AS $$
493
+ BEGIN
494
+ -- Validate graph rules
495
+ IF p_graph_rules IS NOT NULL AND p_graph_rules != '{}' THEN
496
+ IF NOT dzql.validate_graph_rules(p_graph_rules) THEN
497
+ RAISE EXCEPTION 'Invalid graph rules for entity %', p_table_name;
498
+ END IF;
499
+ END IF;
500
+
501
+ -- Update only graph rules
502
+ UPDATE dzql.entities
503
+ SET graph_rules = p_graph_rules
504
+ WHERE table_name = p_table_name;
505
+
506
+ IF NOT FOUND THEN
507
+ RAISE EXCEPTION 'Entity % not found', p_table_name;
508
+ END IF;
509
+
510
+ RAISE NOTICE 'DZQL: Graph rules updated for entity %', p_table_name;
511
+ END $$;
@@ -0,0 +1,83 @@
1
+ -- Authentication System
2
+ -- Simple users table with login/register/profile functions
3
+
4
+ -- Enable pgcrypto extension for password hashing
5
+ create extension if not exists pgcrypto;
6
+
7
+ -- === Users Table ===
8
+ create table if not exists users (
9
+ id serial primary key,
10
+ email text unique not null,
11
+ name text not null,
12
+ password_hash text not null,
13
+ created_at timestamptz default now()
14
+ );
15
+
16
+ -- === Auth Functions ===
17
+
18
+ -- Register new user
19
+ create or replace function register_user(p_email text, p_password text)
20
+ returns jsonb
21
+ language plpgsql
22
+ security definer
23
+ as $$
24
+ declare
25
+ user_id int;
26
+ salt text;
27
+ hash text;
28
+ begin
29
+ -- Generate salt and hash password
30
+ salt := gen_salt('bf', 10);
31
+ hash := crypt(p_password, salt);
32
+
33
+ -- Insert user
34
+ insert into users (email, name, password_hash)
35
+ values (p_email, split_part(p_email, '@', 1), hash)
36
+ returning id into user_id;
37
+
38
+ return _profile(user_id);
39
+ exception
40
+ when unique_violation then
41
+ raise exception 'Email already exists' using errcode = '23505';
42
+ end $$;
43
+
44
+ -- Login user
45
+ create or replace function login_user(p_email text, p_password text)
46
+ returns jsonb
47
+ language plpgsql
48
+ security definer
49
+ as $$
50
+ declare
51
+ user_record record;
52
+ begin
53
+ select id, email, name, password_hash
54
+ into user_record
55
+ from users
56
+ where email = p_email;
57
+
58
+ if not found then
59
+ raise exception 'Invalid credentials' using errcode = '28000';
60
+ end if;
61
+
62
+ if not (user_record.password_hash = crypt(p_password, user_record.password_hash)) then
63
+ raise exception 'Invalid credentials' using errcode = '28000';
64
+ end if;
65
+
66
+ return _profile(user_record.id);
67
+ end $$;
68
+
69
+ -- Get user profile (private function)
70
+ create or replace function _profile(p_user_id int)
71
+ returns jsonb
72
+ language sql
73
+ security definer
74
+ as $$
75
+ select jsonb_build_object(
76
+ 'user_id', id,
77
+ 'email', email,
78
+ 'name', name,
79
+ 'created_at', created_at
80
+ )
81
+ from users
82
+ where id = p_user_id;
83
+ $$;