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.
- package/package.json +65 -0
- package/src/client/ui-configs/sample-2.js +207 -0
- package/src/client/ui-loader.js +618 -0
- package/src/client/ui.js +990 -0
- package/src/client/ws.js +352 -0
- package/src/client.js +9 -0
- package/src/database/migrations/001_schema.sql +59 -0
- package/src/database/migrations/002_functions.sql +742 -0
- package/src/database/migrations/003_operations.sql +725 -0
- package/src/database/migrations/004_search.sql +505 -0
- package/src/database/migrations/005_entities.sql +511 -0
- package/src/database/migrations/006_auth.sql +83 -0
- package/src/database/migrations/007_events.sql +136 -0
- package/src/database/migrations/008_hello.sql +18 -0
- package/src/database/migrations/008a_meta.sql +165 -0
- package/src/index.js +19 -0
- package/src/server/api.js +9 -0
- package/src/server/db.js +261 -0
- package/src/server/index.js +141 -0
- package/src/server/logger.js +246 -0
- package/src/server/mcp.js +594 -0
- package/src/server/meta-route.js +251 -0
- package/src/server/ws.js +464 -0
|
@@ -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
|
+
$$;
|