dzql 0.5.26 → 0.5.28
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
|
@@ -247,6 +247,70 @@ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;`;
|
|
|
247
247
|
scopeTables: scopeTables
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
+
// Pure collection mode: no rootEntity, only relations
|
|
251
|
+
if (!this.rootEntity) {
|
|
252
|
+
// Build relation-only selects (strip leading comma)
|
|
253
|
+
const collectionSelects = this._generateCollectionOnlySelects();
|
|
254
|
+
|
|
255
|
+
return `CREATE OR REPLACE FUNCTION get_${this.name}(
|
|
256
|
+
p_params JSONB,
|
|
257
|
+
p_user_id INT
|
|
258
|
+
) RETURNS JSONB AS $$
|
|
259
|
+
DECLARE
|
|
260
|
+
v_data JSONB;
|
|
261
|
+
BEGIN
|
|
262
|
+
-- Check access control
|
|
263
|
+
IF NOT ${this.name}_can_subscribe(p_user_id, p_params) THEN
|
|
264
|
+
RAISE EXCEPTION 'Permission denied';
|
|
265
|
+
END IF;
|
|
266
|
+
|
|
267
|
+
-- Build document with collections only (no root entity)
|
|
268
|
+
SELECT jsonb_build_object(
|
|
269
|
+
${collectionSelects}
|
|
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
|
+
|
|
282
|
+
// Dashboard mode: empty paramSchema with rootEntity means aggregate root as array
|
|
283
|
+
if (params.length === 0) {
|
|
284
|
+
return `CREATE OR REPLACE FUNCTION get_${this.name}(
|
|
285
|
+
p_params JSONB,
|
|
286
|
+
p_user_id INT
|
|
287
|
+
) RETURNS JSONB AS $$
|
|
288
|
+
DECLARE
|
|
289
|
+
v_data JSONB;
|
|
290
|
+
BEGIN
|
|
291
|
+
-- Check access control
|
|
292
|
+
IF NOT ${this.name}_can_subscribe(p_user_id, p_params) THEN
|
|
293
|
+
RAISE EXCEPTION 'Permission denied';
|
|
294
|
+
END IF;
|
|
295
|
+
|
|
296
|
+
-- Build document with root as array (dashboard mode)
|
|
297
|
+
SELECT jsonb_build_object(
|
|
298
|
+
'${this.rootEntity}', COALESCE((
|
|
299
|
+
SELECT jsonb_agg(row_to_json(root.*))
|
|
300
|
+
FROM ${this.rootEntity} root
|
|
301
|
+
), '[]'::jsonb)${relationSelects}
|
|
302
|
+
)
|
|
303
|
+
INTO v_data;
|
|
304
|
+
|
|
305
|
+
-- Return data with embedded schema for atomic updates
|
|
306
|
+
RETURN jsonb_build_object(
|
|
307
|
+
'data', v_data,
|
|
308
|
+
'schema', '${schemaJson}'::jsonb
|
|
309
|
+
);
|
|
310
|
+
END;
|
|
311
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
312
|
+
}
|
|
313
|
+
|
|
250
314
|
return `CREATE OR REPLACE FUNCTION get_${this.name}(
|
|
251
315
|
p_params JSONB,
|
|
252
316
|
p_user_id INT
|
|
@@ -356,6 +420,33 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
356
420
|
return selects;
|
|
357
421
|
}
|
|
358
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Generate collection-only selects (no root entity)
|
|
425
|
+
* Used when rootEntity is null/empty - pure collection mode
|
|
426
|
+
* @private
|
|
427
|
+
*/
|
|
428
|
+
_generateCollectionOnlySelects() {
|
|
429
|
+
if (Object.keys(this.relations).length === 0) {
|
|
430
|
+
return "'_empty', '{}'::jsonb";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const selects = Object.entries(this.relations).map(([relName, relConfig]) => {
|
|
434
|
+
const relEntity = typeof relConfig === 'string' ? relConfig : relConfig.entity;
|
|
435
|
+
const relFilter = typeof relConfig === 'object' ? relConfig.filter : null;
|
|
436
|
+
|
|
437
|
+
// For collection mode, filter should be TRUE or we fetch all
|
|
438
|
+
const filterSQL = relFilter === 'TRUE' ? 'TRUE' : 'TRUE';
|
|
439
|
+
|
|
440
|
+
return `'${relName}', COALESCE((
|
|
441
|
+
SELECT jsonb_agg(row_to_json(rel.*))
|
|
442
|
+
FROM ${relEntity} rel
|
|
443
|
+
WHERE ${filterSQL}
|
|
444
|
+
), '[]'::jsonb)`;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return selects.join(',\n ');
|
|
448
|
+
}
|
|
449
|
+
|
|
359
450
|
/**
|
|
360
451
|
* Generate JOIN clause for via relations
|
|
361
452
|
* Handles multi-hop via chains by looking up each intermediate table
|
|
@@ -478,13 +569,21 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
|
|
|
478
569
|
*/
|
|
479
570
|
_generateAffectedDocumentsFunction() {
|
|
480
571
|
const cases = [];
|
|
572
|
+
const seenEntities = new Set();
|
|
481
573
|
|
|
482
|
-
// Case 1: Root entity changed
|
|
483
|
-
|
|
574
|
+
// Case 1: Root entity changed (only if rootEntity exists)
|
|
575
|
+
if (this.rootEntity) {
|
|
576
|
+
cases.push(this._generateRootAffectedCase());
|
|
577
|
+
seenEntities.add(this.rootEntity);
|
|
578
|
+
}
|
|
484
579
|
|
|
485
|
-
// Case 2: Related entities changed
|
|
580
|
+
// Case 2: Related entities changed (skip duplicates)
|
|
486
581
|
for (const [relName, relConfig] of Object.entries(this.relations)) {
|
|
487
|
-
|
|
582
|
+
const relEntity = typeof relConfig === 'string' ? relConfig : relConfig.entity;
|
|
583
|
+
if (!seenEntities.has(relEntity)) {
|
|
584
|
+
cases.push(this._generateRelationAffectedCase(relName, relConfig));
|
|
585
|
+
seenEntities.add(relEntity);
|
|
586
|
+
}
|
|
488
587
|
}
|
|
489
588
|
|
|
490
589
|
const casesSQL = cases.join('\n\n ');
|
|
@@ -515,7 +614,15 @@ $$ LANGUAGE plpgsql IMMUTABLE;`;
|
|
|
515
614
|
*/
|
|
516
615
|
_generateRootAffectedCase() {
|
|
517
616
|
const params = Object.keys(this.paramSchema);
|
|
518
|
-
|
|
617
|
+
|
|
618
|
+
// Dashboard mode: empty paramSchema means notify ALL subscribers
|
|
619
|
+
if (params.length === 0) {
|
|
620
|
+
return `-- Root entity (${this.rootEntity}) changed - dashboard mode, notify all
|
|
621
|
+
WHEN '${this.rootEntity}' THEN
|
|
622
|
+
v_affected := ARRAY['{}'::jsonb];`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const firstParam = params[0];
|
|
519
626
|
|
|
520
627
|
return `-- Root entity (${this.rootEntity}) changed
|
|
521
628
|
WHEN '${this.rootEntity}' THEN
|
|
@@ -618,7 +725,10 @@ $$ LANGUAGE plpgsql IMMUTABLE;`;
|
|
|
618
725
|
* @returns {string[]} Array of table names
|
|
619
726
|
*/
|
|
620
727
|
extractScopeTables() {
|
|
621
|
-
const tables = new Set(
|
|
728
|
+
const tables = new Set();
|
|
729
|
+
if (this.rootEntity) {
|
|
730
|
+
tables.add(this.rootEntity);
|
|
731
|
+
}
|
|
622
732
|
|
|
623
733
|
const extractFromRelations = (relations) => {
|
|
624
734
|
for (const [relName, relConfig] of Object.entries(relations || {})) {
|
|
@@ -655,8 +765,10 @@ $$ LANGUAGE plpgsql IMMUTABLE;`;
|
|
|
655
765
|
buildPathMapping() {
|
|
656
766
|
const paths = {};
|
|
657
767
|
|
|
658
|
-
// Root entity maps to top level
|
|
659
|
-
|
|
768
|
+
// Root entity maps to top level (only if it exists)
|
|
769
|
+
if (this.rootEntity) {
|
|
770
|
+
paths[this.rootEntity] = '.';
|
|
771
|
+
}
|
|
660
772
|
|
|
661
773
|
const buildPaths = (relations, parentPath = '') => {
|
|
662
774
|
for (const [relName, relConfig] of Object.entries(relations || {})) {
|