dzql 0.2.3 → 0.3.0
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/docs/guides/custom-functions.md +362 -0
- package/docs/guides/field-defaults.md +240 -0
- package/docs/guides/many-to-many.md +894 -0
- package/docs/reference/api.md +147 -3
- package/package.json +2 -2
- package/src/compiler/compiler.js +23 -13
- package/src/compiler/parser/entity-parser.js +74 -14
- package/src/database/migrations/001_schema.sql +3 -1
- package/src/database/migrations/002_functions.sql +5 -0
- package/src/database/migrations/003_operations.sql +236 -1
- package/src/database/migrations/004_search.sql +64 -0
- package/src/database/migrations/005_entities.sql +11 -4
|
@@ -223,6 +223,62 @@ BEGIN
|
|
|
223
223
|
END LOOP;
|
|
224
224
|
END IF;
|
|
225
225
|
|
|
226
|
+
-- Expand many-to-many relationships (if configured)
|
|
227
|
+
IF l_entity_config.many_to_many IS NOT NULL AND l_entity_config.many_to_many != '{}'::jsonb THEN
|
|
228
|
+
DECLARE
|
|
229
|
+
l_m2m_key text;
|
|
230
|
+
l_m2m_config jsonb;
|
|
231
|
+
l_id_field text;
|
|
232
|
+
l_junction_table text;
|
|
233
|
+
l_local_key text;
|
|
234
|
+
l_foreign_key text;
|
|
235
|
+
l_target_entity text;
|
|
236
|
+
l_expand boolean;
|
|
237
|
+
l_record_id text;
|
|
238
|
+
l_id_array jsonb;
|
|
239
|
+
l_expanded_objects jsonb;
|
|
240
|
+
BEGIN
|
|
241
|
+
-- Get the primary key value from the result
|
|
242
|
+
l_record_id := l_result->>l_pk_cols[1]; -- Assume single PK for now
|
|
243
|
+
|
|
244
|
+
FOR l_m2m_key IN SELECT jsonb_object_keys(l_entity_config.many_to_many)
|
|
245
|
+
LOOP
|
|
246
|
+
l_m2m_config := l_entity_config.many_to_many->l_m2m_key;
|
|
247
|
+
l_id_field := l_m2m_config->>'id_field';
|
|
248
|
+
l_junction_table := l_m2m_config->>'junction_table';
|
|
249
|
+
l_local_key := l_m2m_config->>'local_key';
|
|
250
|
+
l_foreign_key := l_m2m_config->>'foreign_key';
|
|
251
|
+
l_target_entity := l_m2m_config->>'target_entity';
|
|
252
|
+
l_expand := COALESCE((l_m2m_config->>'expand')::boolean, false);
|
|
253
|
+
|
|
254
|
+
-- Always include array of IDs
|
|
255
|
+
EXECUTE format('
|
|
256
|
+
SELECT COALESCE(jsonb_agg(%I), ''[]''::jsonb)
|
|
257
|
+
FROM %I
|
|
258
|
+
WHERE %I = $1::int
|
|
259
|
+
', l_foreign_key, l_junction_table, l_local_key)
|
|
260
|
+
INTO l_id_array
|
|
261
|
+
USING l_record_id;
|
|
262
|
+
|
|
263
|
+
l_result := l_result || jsonb_build_object(l_id_field, l_id_array);
|
|
264
|
+
|
|
265
|
+
-- Conditionally include expanded objects if expand: true
|
|
266
|
+
IF l_expand THEN
|
|
267
|
+
EXECUTE format('
|
|
268
|
+
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
269
|
+
FROM %I jt
|
|
270
|
+
JOIN %I t ON t.id = jt.%I
|
|
271
|
+
WHERE jt.%I = $1::int
|
|
272
|
+
', l_junction_table, l_target_entity, l_foreign_key, l_local_key)
|
|
273
|
+
INTO l_expanded_objects
|
|
274
|
+
USING l_record_id;
|
|
275
|
+
|
|
276
|
+
l_result := l_result || jsonb_build_object(l_m2m_key, l_expanded_objects);
|
|
277
|
+
END IF;
|
|
278
|
+
END LOOP;
|
|
279
|
+
END;
|
|
280
|
+
END IF;
|
|
281
|
+
|
|
226
282
|
RETURN l_result;
|
|
227
283
|
END $$;
|
|
228
284
|
|
|
@@ -333,7 +389,29 @@ BEGIN
|
|
|
333
389
|
LOOP
|
|
334
390
|
-- Don't update any primary key columns
|
|
335
391
|
IF NOT (l_col_name = ANY(l_pk_cols)) THEN
|
|
336
|
-
|
|
392
|
+
-- Skip M2M ID fields (they're not real table columns)
|
|
393
|
+
IF l_entity_config.many_to_many IS NOT NULL THEN
|
|
394
|
+
DECLARE
|
|
395
|
+
l_m2m_id_field text;
|
|
396
|
+
l_skip boolean := false;
|
|
397
|
+
BEGIN
|
|
398
|
+
FOR l_m2m_id_field IN
|
|
399
|
+
SELECT value->>'id_field'
|
|
400
|
+
FROM jsonb_each(l_entity_config.many_to_many)
|
|
401
|
+
LOOP
|
|
402
|
+
IF l_col_name = l_m2m_id_field THEN
|
|
403
|
+
l_skip := true;
|
|
404
|
+
EXIT;
|
|
405
|
+
END IF;
|
|
406
|
+
END LOOP;
|
|
407
|
+
|
|
408
|
+
IF NOT l_skip THEN
|
|
409
|
+
l_set_clauses := l_set_clauses || format('%I = %L', l_col_name, l_merged_data ->> l_col_name);
|
|
410
|
+
END IF;
|
|
411
|
+
END;
|
|
412
|
+
ELSE
|
|
413
|
+
l_set_clauses := l_set_clauses || format('%I = %L', l_col_name, l_merged_data ->> l_col_name);
|
|
414
|
+
END IF;
|
|
337
415
|
END IF;
|
|
338
416
|
END LOOP;
|
|
339
417
|
|
|
@@ -359,6 +437,38 @@ BEGIN
|
|
|
359
437
|
l_args_json := l_args_json || jsonb_build_object('user_id', p_user_id);
|
|
360
438
|
END IF;
|
|
361
439
|
|
|
440
|
+
-- Apply field defaults for INSERT (if configured)
|
|
441
|
+
IF l_entity_config.field_defaults IS NOT NULL AND l_entity_config.field_defaults != '{}' THEN
|
|
442
|
+
FOR l_col_name IN SELECT jsonb_object_keys(l_entity_config.field_defaults)
|
|
443
|
+
LOOP
|
|
444
|
+
-- Only apply default if field is not already provided
|
|
445
|
+
IF NOT (l_args_json ? l_col_name) THEN
|
|
446
|
+
DECLARE
|
|
447
|
+
l_default_value text;
|
|
448
|
+
l_resolved_value text;
|
|
449
|
+
BEGIN
|
|
450
|
+
l_default_value := l_entity_config.field_defaults->>l_col_name;
|
|
451
|
+
|
|
452
|
+
-- Resolve variable if it starts with @
|
|
453
|
+
IF l_default_value LIKE '@%' THEN
|
|
454
|
+
l_resolved_value := dzql.resolve_graph_variable(
|
|
455
|
+
l_default_value,
|
|
456
|
+
NULL, -- no before record for INSERT
|
|
457
|
+
l_args_json, -- current data being inserted
|
|
458
|
+
p_user_id
|
|
459
|
+
);
|
|
460
|
+
ELSE
|
|
461
|
+
-- Use literal value
|
|
462
|
+
l_resolved_value := l_default_value;
|
|
463
|
+
END IF;
|
|
464
|
+
|
|
465
|
+
-- Add to l_args_json
|
|
466
|
+
l_args_json := l_args_json || jsonb_build_object(l_col_name, l_resolved_value);
|
|
467
|
+
END;
|
|
468
|
+
END IF;
|
|
469
|
+
END LOOP;
|
|
470
|
+
END IF;
|
|
471
|
+
|
|
362
472
|
-- Check create permission on new values
|
|
363
473
|
l_operation := 'create';
|
|
364
474
|
l_permission_record := l_args_json;
|
|
@@ -371,6 +481,28 @@ BEGIN
|
|
|
371
481
|
|
|
372
482
|
FOR l_col_name IN SELECT jsonb_object_keys(l_args_json)
|
|
373
483
|
LOOP
|
|
484
|
+
-- Skip M2M ID fields (they're not real table columns)
|
|
485
|
+
IF l_entity_config.many_to_many IS NOT NULL THEN
|
|
486
|
+
DECLARE
|
|
487
|
+
l_m2m_id_field text;
|
|
488
|
+
l_skip boolean := false;
|
|
489
|
+
BEGIN
|
|
490
|
+
FOR l_m2m_id_field IN
|
|
491
|
+
SELECT value->>'id_field'
|
|
492
|
+
FROM jsonb_each(l_entity_config.many_to_many)
|
|
493
|
+
LOOP
|
|
494
|
+
IF l_col_name = l_m2m_id_field THEN
|
|
495
|
+
l_skip := true;
|
|
496
|
+
EXIT;
|
|
497
|
+
END IF;
|
|
498
|
+
END LOOP;
|
|
499
|
+
|
|
500
|
+
IF l_skip THEN
|
|
501
|
+
CONTINUE;
|
|
502
|
+
END IF;
|
|
503
|
+
END;
|
|
504
|
+
END IF;
|
|
505
|
+
|
|
374
506
|
IF l_args_json ->> l_col_name IS NOT NULL AND l_args_json ->> l_col_name != '' THEN
|
|
375
507
|
l_cols := l_cols || quote_ident(l_col_name);
|
|
376
508
|
l_vals := l_vals || quote_literal(l_args_json ->> l_col_name);
|
|
@@ -391,6 +523,109 @@ BEGIN
|
|
|
391
523
|
|
|
392
524
|
END IF;
|
|
393
525
|
|
|
526
|
+
-- Sync many-to-many relationships (if configured)
|
|
527
|
+
IF l_entity_config.many_to_many IS NOT NULL AND l_entity_config.many_to_many != '{}'::jsonb THEN
|
|
528
|
+
DECLARE
|
|
529
|
+
l_m2m_key text;
|
|
530
|
+
l_m2m_config jsonb;
|
|
531
|
+
l_id_field text;
|
|
532
|
+
l_junction_table text;
|
|
533
|
+
l_local_key text;
|
|
534
|
+
l_foreign_key text;
|
|
535
|
+
l_record_id text;
|
|
536
|
+
BEGIN
|
|
537
|
+
-- Get the primary key value from the result
|
|
538
|
+
l_record_id := l_result->>l_pk_cols[1]; -- Assume single PK for now
|
|
539
|
+
|
|
540
|
+
FOR l_m2m_key IN SELECT jsonb_object_keys(l_entity_config.many_to_many)
|
|
541
|
+
LOOP
|
|
542
|
+
l_m2m_config := l_entity_config.many_to_many->l_m2m_key;
|
|
543
|
+
l_id_field := l_m2m_config->>'id_field';
|
|
544
|
+
|
|
545
|
+
-- Only sync if the ID field is present in the data
|
|
546
|
+
IF l_args_json ? l_id_field THEN
|
|
547
|
+
l_junction_table := l_m2m_config->>'junction_table';
|
|
548
|
+
l_local_key := l_m2m_config->>'local_key';
|
|
549
|
+
l_foreign_key := l_m2m_config->>'foreign_key';
|
|
550
|
+
|
|
551
|
+
-- Delete relationships not in new list
|
|
552
|
+
EXECUTE format('
|
|
553
|
+
DELETE FROM %I
|
|
554
|
+
WHERE %I = $1::int
|
|
555
|
+
AND %I <> ALL($2::int[])
|
|
556
|
+
', l_junction_table, l_local_key, l_foreign_key)
|
|
557
|
+
USING l_record_id,
|
|
558
|
+
ARRAY(SELECT jsonb_array_elements_text(l_args_json->l_id_field))::int[];
|
|
559
|
+
|
|
560
|
+
-- Insert new relationships (ignore conflicts)
|
|
561
|
+
EXECUTE format('
|
|
562
|
+
INSERT INTO %I (%I, %I)
|
|
563
|
+
SELECT $1::int, value::int
|
|
564
|
+
FROM jsonb_array_elements_text($2)
|
|
565
|
+
ON CONFLICT DO NOTHING
|
|
566
|
+
', l_junction_table, l_local_key, l_foreign_key)
|
|
567
|
+
USING l_record_id, l_args_json->l_id_field;
|
|
568
|
+
END IF;
|
|
569
|
+
END LOOP;
|
|
570
|
+
END;
|
|
571
|
+
END IF;
|
|
572
|
+
|
|
573
|
+
-- Expand many-to-many relationships in result (after sync)
|
|
574
|
+
IF l_entity_config.many_to_many IS NOT NULL AND l_entity_config.many_to_many != '{}'::jsonb THEN
|
|
575
|
+
DECLARE
|
|
576
|
+
l_m2m_key text;
|
|
577
|
+
l_m2m_config jsonb;
|
|
578
|
+
l_id_field text;
|
|
579
|
+
l_junction_table text;
|
|
580
|
+
l_local_key text;
|
|
581
|
+
l_foreign_key text;
|
|
582
|
+
l_target_entity text;
|
|
583
|
+
l_expand boolean;
|
|
584
|
+
l_record_id text;
|
|
585
|
+
l_id_array jsonb;
|
|
586
|
+
l_expanded_objects jsonb;
|
|
587
|
+
BEGIN
|
|
588
|
+
-- Get the primary key value from the result
|
|
589
|
+
l_record_id := l_result->>l_pk_cols[1]; -- Assume single PK for now
|
|
590
|
+
|
|
591
|
+
FOR l_m2m_key IN SELECT jsonb_object_keys(l_entity_config.many_to_many)
|
|
592
|
+
LOOP
|
|
593
|
+
l_m2m_config := l_entity_config.many_to_many->l_m2m_key;
|
|
594
|
+
l_id_field := l_m2m_config->>'id_field';
|
|
595
|
+
l_junction_table := l_m2m_config->>'junction_table';
|
|
596
|
+
l_local_key := l_m2m_config->>'local_key';
|
|
597
|
+
l_foreign_key := l_m2m_config->>'foreign_key';
|
|
598
|
+
l_target_entity := l_m2m_config->>'target_entity';
|
|
599
|
+
l_expand := COALESCE((l_m2m_config->>'expand')::boolean, false);
|
|
600
|
+
|
|
601
|
+
-- Always include array of IDs
|
|
602
|
+
EXECUTE format('
|
|
603
|
+
SELECT COALESCE(jsonb_agg(%I), ''[]''::jsonb)
|
|
604
|
+
FROM %I
|
|
605
|
+
WHERE %I = $1::int
|
|
606
|
+
', l_foreign_key, l_junction_table, l_local_key)
|
|
607
|
+
INTO l_id_array
|
|
608
|
+
USING l_record_id;
|
|
609
|
+
|
|
610
|
+
l_result := l_result || jsonb_build_object(l_id_field, l_id_array);
|
|
611
|
+
|
|
612
|
+
-- Conditionally include expanded objects if expand: true
|
|
613
|
+
IF l_expand THEN
|
|
614
|
+
EXECUTE format('
|
|
615
|
+
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
616
|
+
FROM %I jt
|
|
617
|
+
JOIN %I t ON t.id = jt.%I
|
|
618
|
+
WHERE jt.%I = $1::int
|
|
619
|
+
', l_junction_table, l_target_entity, l_foreign_key, l_local_key)
|
|
620
|
+
INTO l_expanded_objects
|
|
621
|
+
USING l_record_id;
|
|
622
|
+
|
|
623
|
+
l_result := l_result || jsonb_build_object(l_m2m_key, l_expanded_objects);
|
|
624
|
+
END IF;
|
|
625
|
+
END LOOP;
|
|
626
|
+
END;
|
|
627
|
+
END IF;
|
|
628
|
+
|
|
394
629
|
-- Execute graph rules for the appropriate operation
|
|
395
630
|
l_graph_rules_result := dzql.execute_graph_rules(
|
|
396
631
|
p_entity,
|
|
@@ -398,6 +398,70 @@ BEGIN
|
|
|
398
398
|
END IF;
|
|
399
399
|
END LOOP;
|
|
400
400
|
|
|
401
|
+
-- Expand many-to-many relationships for this record (if configured)
|
|
402
|
+
IF l_entity_config.many_to_many IS NOT NULL AND l_entity_config.many_to_many != '{}'::jsonb THEN
|
|
403
|
+
DECLARE
|
|
404
|
+
l_m2m_key text;
|
|
405
|
+
l_m2m_config jsonb;
|
|
406
|
+
l_id_field text;
|
|
407
|
+
l_junction_table text;
|
|
408
|
+
l_local_key text;
|
|
409
|
+
l_foreign_key text;
|
|
410
|
+
l_target_entity text;
|
|
411
|
+
l_expand boolean;
|
|
412
|
+
l_record_id text;
|
|
413
|
+
l_id_array jsonb;
|
|
414
|
+
l_expanded_objects jsonb;
|
|
415
|
+
l_pk_cols text[];
|
|
416
|
+
BEGIN
|
|
417
|
+
-- Get primary key columns for this entity
|
|
418
|
+
SELECT array_agg(a.attname ORDER BY a.attnum)
|
|
419
|
+
INTO l_pk_cols
|
|
420
|
+
FROM pg_index idx
|
|
421
|
+
JOIN pg_attribute a ON a.attrelid = idx.indrelid AND a.attnum = ANY(idx.indkey)
|
|
422
|
+
WHERE idx.indrelid = p_entity::regclass AND idx.indisprimary;
|
|
423
|
+
|
|
424
|
+
-- Get the primary key value from the record
|
|
425
|
+
l_record_id := l_record->>l_pk_cols[1]; -- Assume single PK for now
|
|
426
|
+
|
|
427
|
+
FOR l_m2m_key IN SELECT jsonb_object_keys(l_entity_config.many_to_many)
|
|
428
|
+
LOOP
|
|
429
|
+
l_m2m_config := l_entity_config.many_to_many->l_m2m_key;
|
|
430
|
+
l_id_field := l_m2m_config->>'id_field';
|
|
431
|
+
l_junction_table := l_m2m_config->>'junction_table';
|
|
432
|
+
l_local_key := l_m2m_config->>'local_key';
|
|
433
|
+
l_foreign_key := l_m2m_config->>'foreign_key';
|
|
434
|
+
l_target_entity := l_m2m_config->>'target_entity';
|
|
435
|
+
l_expand := COALESCE((l_m2m_config->>'expand')::boolean, false);
|
|
436
|
+
|
|
437
|
+
-- Always include array of IDs
|
|
438
|
+
EXECUTE format('
|
|
439
|
+
SELECT COALESCE(jsonb_agg(%I), ''[]''::jsonb)
|
|
440
|
+
FROM %I
|
|
441
|
+
WHERE %I = $1::int
|
|
442
|
+
', l_foreign_key, l_junction_table, l_local_key)
|
|
443
|
+
INTO l_id_array
|
|
444
|
+
USING l_record_id;
|
|
445
|
+
|
|
446
|
+
l_record := l_record || jsonb_build_object(l_id_field, l_id_array);
|
|
447
|
+
|
|
448
|
+
-- Conditionally include expanded objects if expand: true
|
|
449
|
+
IF l_expand THEN
|
|
450
|
+
EXECUTE format('
|
|
451
|
+
SELECT COALESCE(jsonb_agg(to_jsonb(t.*)), ''[]''::jsonb)
|
|
452
|
+
FROM %I jt
|
|
453
|
+
JOIN %I t ON t.id = jt.%I
|
|
454
|
+
WHERE jt.%I = $1::int
|
|
455
|
+
', l_junction_table, l_target_entity, l_foreign_key, l_local_key)
|
|
456
|
+
INTO l_expanded_objects
|
|
457
|
+
USING l_record_id;
|
|
458
|
+
|
|
459
|
+
l_record := l_record || jsonb_build_object(l_m2m_key, l_expanded_objects);
|
|
460
|
+
END IF;
|
|
461
|
+
END LOOP;
|
|
462
|
+
END;
|
|
463
|
+
END IF;
|
|
464
|
+
|
|
401
465
|
l_processed_data := l_processed_data || l_record;
|
|
402
466
|
END LOOP;
|
|
403
467
|
|
|
@@ -579,7 +579,8 @@ CREATE OR REPLACE FUNCTION dzql.register_entity(
|
|
|
579
579
|
p_temporal_fields jsonb DEFAULT '{}',
|
|
580
580
|
p_notification_paths jsonb DEFAULT '{}',
|
|
581
581
|
p_permission_paths jsonb DEFAULT '{}',
|
|
582
|
-
p_graph_rules jsonb DEFAULT '{}'
|
|
582
|
+
p_graph_rules jsonb DEFAULT '{}',
|
|
583
|
+
p_field_defaults jsonb DEFAULT '{}'
|
|
583
584
|
) RETURNS void
|
|
584
585
|
LANGUAGE plpgsql AS $$
|
|
585
586
|
DECLARE
|
|
@@ -587,6 +588,7 @@ DECLARE
|
|
|
587
588
|
l_rule_name text;
|
|
588
589
|
l_rule_config jsonb;
|
|
589
590
|
l_action jsonb;
|
|
591
|
+
l_many_to_many jsonb;
|
|
590
592
|
BEGIN
|
|
591
593
|
-- Validate permission paths if provided
|
|
592
594
|
IF p_permission_paths IS NOT NULL AND p_permission_paths != '{}' THEN
|
|
@@ -602,11 +604,14 @@ BEGIN
|
|
|
602
604
|
END IF;
|
|
603
605
|
END IF;
|
|
604
606
|
|
|
607
|
+
-- Extract many_to_many from graph_rules if present
|
|
608
|
+
l_many_to_many := COALESCE(p_graph_rules->'many_to_many', '{}'::jsonb);
|
|
609
|
+
|
|
605
610
|
-- Insert or update entity configuration
|
|
606
611
|
INSERT INTO dzql.entities
|
|
607
|
-
(table_name, label_field, searchable_fields, fk_includes, soft_delete, temporal_fields, notification_paths, permission_paths, graph_rules)
|
|
612
|
+
(table_name, label_field, searchable_fields, fk_includes, soft_delete, temporal_fields, notification_paths, permission_paths, graph_rules, field_defaults, many_to_many)
|
|
608
613
|
VALUES
|
|
609
|
-
(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)
|
|
614
|
+
(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, p_field_defaults, l_many_to_many)
|
|
610
615
|
ON CONFLICT (table_name) DO UPDATE SET
|
|
611
616
|
label_field = EXCLUDED.label_field,
|
|
612
617
|
searchable_fields = EXCLUDED.searchable_fields,
|
|
@@ -615,7 +620,9 @@ BEGIN
|
|
|
615
620
|
temporal_fields = EXCLUDED.temporal_fields,
|
|
616
621
|
notification_paths = EXCLUDED.notification_paths,
|
|
617
622
|
permission_paths = EXCLUDED.permission_paths,
|
|
618
|
-
graph_rules = EXCLUDED.graph_rules
|
|
623
|
+
graph_rules = EXCLUDED.graph_rules,
|
|
624
|
+
field_defaults = EXCLUDED.field_defaults,
|
|
625
|
+
many_to_many = EXCLUDED.many_to_many;
|
|
619
626
|
|
|
620
627
|
-- Create API functions for this entity
|
|
621
628
|
PERFORM dzql.create_entity_functions(p_table_name);
|