dzql 0.5.25 → 0.5.27

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.5.25",
3
+ "version": "0.5.27",
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",
@@ -247,6 +247,38 @@ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
247
247
  scopeTables: scopeTables
248
248
  });
249
249
 
250
+ // Dashboard mode: empty paramSchema means aggregate root as array
251
+ if (params.length === 0) {
252
+ return `CREATE OR REPLACE FUNCTION get_${this.name}(
253
+ p_params JSONB,
254
+ p_user_id INT
255
+ ) RETURNS JSONB AS $$
256
+ DECLARE
257
+ v_data JSONB;
258
+ BEGIN
259
+ -- Check access control
260
+ IF NOT ${this.name}_can_subscribe(p_user_id, p_params) THEN
261
+ RAISE EXCEPTION 'Permission denied';
262
+ END IF;
263
+
264
+ -- Build document with root as array (dashboard mode)
265
+ SELECT jsonb_build_object(
266
+ '${this.rootEntity}', COALESCE((
267
+ SELECT jsonb_agg(row_to_json(root.*))
268
+ FROM ${this.rootEntity} root
269
+ ), '[]'::jsonb)${relationSelects}
270
+ )
271
+ INTO v_data;
272
+
273
+ -- Return data with embedded schema for atomic updates
274
+ RETURN jsonb_build_object(
275
+ 'data', v_data,
276
+ 'schema', '${schemaJson}'::jsonb
277
+ );
278
+ END;
279
+ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
280
+ }
281
+
250
282
  return `CREATE OR REPLACE FUNCTION get_${this.name}(
251
283
  p_params JSONB,
252
284
  p_user_id INT
@@ -462,6 +494,11 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
462
494
  return `rel.${fk} = root.id`;
463
495
  }
464
496
 
497
+ // Dashboard collection: filter="TRUE" means fetch ALL rows (no FK filter)
498
+ if (filter === 'TRUE') {
499
+ return 'TRUE';
500
+ }
501
+
465
502
  // Parse filter expression like "venue_id=$venue_id"
466
503
  // Replace $param with v_param variable
467
504
  return filter.replace(/\$(\w+)/g, 'v_$1');
@@ -473,13 +510,19 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
473
510
  */
474
511
  _generateAffectedDocumentsFunction() {
475
512
  const cases = [];
513
+ const seenEntities = new Set();
476
514
 
477
515
  // Case 1: Root entity changed
478
516
  cases.push(this._generateRootAffectedCase());
517
+ seenEntities.add(this.rootEntity);
479
518
 
480
- // Case 2: Related entities changed
519
+ // Case 2: Related entities changed (skip duplicates)
481
520
  for (const [relName, relConfig] of Object.entries(this.relations)) {
482
- cases.push(this._generateRelationAffectedCase(relName, relConfig));
521
+ const relEntity = typeof relConfig === 'string' ? relConfig : relConfig.entity;
522
+ if (!seenEntities.has(relEntity)) {
523
+ cases.push(this._generateRelationAffectedCase(relName, relConfig));
524
+ seenEntities.add(relEntity);
525
+ }
483
526
  }
484
527
 
485
528
  const casesSQL = cases.join('\n\n ');
@@ -510,7 +553,15 @@ $$ LANGUAGE plpgsql IMMUTABLE;`;
510
553
  */
511
554
  _generateRootAffectedCase() {
512
555
  const params = Object.keys(this.paramSchema);
513
- const firstParam = params[0] || 'id';
556
+
557
+ // Dashboard mode: empty paramSchema means notify ALL subscribers
558
+ if (params.length === 0) {
559
+ return `-- Root entity (${this.rootEntity}) changed - dashboard mode, notify all
560
+ WHEN '${this.rootEntity}' THEN
561
+ v_affected := ARRAY['{}'::jsonb];`;
562
+ }
563
+
564
+ const firstParam = params[0];
514
565
 
515
566
  return `-- Root entity (${this.rootEntity}) changed
516
567
  WHEN '${this.rootEntity}' THEN
@@ -529,10 +580,19 @@ $$ LANGUAGE plpgsql IMMUTABLE;`;
529
580
  ? relConfig.foreignKey
530
581
  : `${this.rootEntity}_id`;
531
582
  const relVia = typeof relConfig === 'object' ? relConfig.via : null;
583
+ const relFilter = typeof relConfig === 'object' ? relConfig.filter : null;
532
584
 
533
585
  const params = Object.keys(this.paramSchema);
534
586
  const firstParam = params[0] || 'id';
535
587
 
588
+ // Dashboard collection: filter="TRUE" means notify ALL subscribers
589
+ // This relation is independent from the root entity
590
+ if (relFilter === 'TRUE') {
591
+ return `-- Dashboard collection (${relEntity}) - notify all subscribers
592
+ WHEN '${relEntity}' THEN
593
+ v_affected := ARRAY['{}'::jsonb];`;
594
+ }
595
+
536
596
  // Check if this is a nested relation (has parent FK)
537
597
  const nestedIncludes = typeof relConfig === 'object' ? relConfig.include : null;
538
598