dzql 0.6.14 → 0.6.16
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
package/src/cli/codegen/sql.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { compilePermission } from "../compiler/permissions.js";
|
|
2
2
|
import { compileGraphRules } from "../compiler/graph_rules.js";
|
|
3
|
-
import type { EntityIR, ManyToManyIR } from "../../shared/ir.js";
|
|
3
|
+
import type { EntityIR, ManyToManyIR, IncludeIR } from "../../shared/ir.js";
|
|
4
4
|
|
|
5
5
|
/** Column info from EntityIR */
|
|
6
6
|
interface ColumnInfo {
|
|
@@ -313,6 +313,32 @@ export function generateSaveFunction(name: string, entityIR: EntityIR): string {
|
|
|
313
313
|
return sql;
|
|
314
314
|
}).join('\n');
|
|
315
315
|
|
|
316
|
+
// FK expansion (add related objects to output for real-time events)
|
|
317
|
+
// Only expand direct FKs (where key_id column exists), not reverse FKs (child arrays)
|
|
318
|
+
const includes: Record<string, IncludeIR> = entityIR.includes || {};
|
|
319
|
+
const includeKeys = Object.keys(includes);
|
|
320
|
+
const fkExpansion = includeKeys.map(key => {
|
|
321
|
+
const config: IncludeIR = includes[key];
|
|
322
|
+
const targetEntity = config.entity;
|
|
323
|
+
const fkField = `${key}_id`; // Convention: author -> author_id
|
|
324
|
+
|
|
325
|
+
// Only expand if this is a direct FK (key_id column exists)
|
|
326
|
+
const hasFkColumn = entityIR.columns.some((c: ColumnInfo) => c.name === fkField);
|
|
327
|
+
|
|
328
|
+
if (hasFkColumn) {
|
|
329
|
+
// Direct FK: single object expansion (e.g., author_id -> author object)
|
|
330
|
+
return `
|
|
331
|
+
-- FK: Add ${key} to output (from ${fkField})
|
|
332
|
+
IF (v_result->>'${fkField}') IS NOT NULL THEN
|
|
333
|
+
v_result := v_result || jsonb_build_object('${key}',
|
|
334
|
+
(SELECT to_jsonb(t.*) FROM ${targetEntity} t WHERE t.id = (v_result->>'${fkField}')::int));
|
|
335
|
+
END IF;`;
|
|
336
|
+
}
|
|
337
|
+
// Skip reverse FKs - they would require querying child tables which may not exist
|
|
338
|
+
// and are not needed for the primary use case of expanding the saved record
|
|
339
|
+
return '';
|
|
340
|
+
}).filter(s => s).join('\n');
|
|
341
|
+
|
|
316
342
|
return `
|
|
317
343
|
CREATE OR REPLACE FUNCTION dzql_v2.save_${name}(p_user_id int, p_data jsonb)
|
|
318
344
|
RETURNS jsonb
|
|
@@ -365,6 +391,7 @@ ${m2mExtraction}
|
|
|
365
391
|
END IF;
|
|
366
392
|
${m2mSync}
|
|
367
393
|
${m2mExpansion}
|
|
394
|
+
${fkExpansion}
|
|
368
395
|
|
|
369
396
|
-- Resolve notification recipients
|
|
370
397
|
v_notify_users := dzql_v2.${name}_notify_users(p_user_id, v_result);
|
|
@@ -512,10 +512,18 @@ function generateNestedSelects(
|
|
|
512
512
|
function generateAffectedKeysFunction(name: string, sub: SubscribableIR): string {
|
|
513
513
|
const cases: string[] = [];
|
|
514
514
|
const paramKey = sub.root.key;
|
|
515
|
+
const hasParams = Object.keys(sub.params).length > 0;
|
|
515
516
|
|
|
516
517
|
// Root entity case
|
|
517
|
-
|
|
518
|
+
// For subscribables with no params (list feeds), just return the subscribable name
|
|
519
|
+
// For subscribables with params, include the root entity's id
|
|
520
|
+
if (hasParams) {
|
|
521
|
+
cases.push(` WHEN '${sub.root.entity}' THEN
|
|
518
522
|
RETURN ARRAY['${name}:' || (p_data->>'id')];`);
|
|
523
|
+
} else {
|
|
524
|
+
cases.push(` WHEN '${sub.root.entity}' THEN
|
|
525
|
+
RETURN ARRAY['${name}'];`);
|
|
526
|
+
}
|
|
519
527
|
|
|
520
528
|
// Get singular form of root entity for FK fields
|
|
521
529
|
const singularRootEntity = singularize(sub.root.entity);
|
|
@@ -552,18 +560,30 @@ function generateAffectedKeysFunction(name: string, sub: SubscribableIR): string
|
|
|
552
560
|
const parentRel = relationships[parentEntity]?.[relEntity];
|
|
553
561
|
const fkField = parentRel?.fkOnParent || `${relName}_id`;
|
|
554
562
|
|
|
555
|
-
|
|
563
|
+
if (hasParams) {
|
|
564
|
+
cases.push(` WHEN '${relEntity}' THEN
|
|
556
565
|
-- Nested: traverse via ${parentEntity}.${fkField}
|
|
557
566
|
SELECT ARRAY_AGG('${name}:' || parent.${singularRootEntity}_id)
|
|
558
567
|
INTO v_keys
|
|
559
568
|
FROM ${parentEntity} parent
|
|
560
569
|
WHERE parent.${fkField} = (p_data->>'id')::int;
|
|
561
570
|
RETURN COALESCE(v_keys, ARRAY[]::text[]);`);
|
|
571
|
+
} else {
|
|
572
|
+
// No params - just return the subscribable name
|
|
573
|
+
cases.push(` WHEN '${relEntity}' THEN
|
|
574
|
+
RETURN ARRAY['${name}'];`);
|
|
575
|
+
}
|
|
562
576
|
} else {
|
|
563
577
|
// Direct child of root - use the FK on child that points to root
|
|
564
578
|
const keyField = fkOnChild || `${singularRootEntity}_id`;
|
|
565
|
-
|
|
579
|
+
if (hasParams) {
|
|
580
|
+
cases.push(` WHEN '${relEntity}' THEN
|
|
566
581
|
RETURN ARRAY['${name}:' || (p_data->>'${keyField}')];`);
|
|
582
|
+
} else {
|
|
583
|
+
// No params - just return the subscribable name
|
|
584
|
+
cases.push(` WHEN '${relEntity}' THEN
|
|
585
|
+
RETURN ARRAY['${name}'];`);
|
|
586
|
+
}
|
|
567
587
|
}
|
|
568
588
|
|
|
569
589
|
// Recurse for nested includes
|
package/src/cli/compiler/ir.ts
CHANGED
|
@@ -175,6 +175,9 @@ export function generateIR(domain: DomainConfig): DomainIR {
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// Parse includes (FK expansions)
|
|
179
|
+
const includes = parseIncludes(config.includes as Record<string, string | IncludeConfig> | undefined);
|
|
180
|
+
|
|
178
181
|
entities[name] = {
|
|
179
182
|
name,
|
|
180
183
|
table: name,
|
|
@@ -187,6 +190,7 @@ export function generateIR(domain: DomainConfig): DomainIR {
|
|
|
187
190
|
fieldDefaults: config.fieldDefaults || {},
|
|
188
191
|
permissions,
|
|
189
192
|
relationships: {},
|
|
193
|
+
includes,
|
|
190
194
|
manyToMany,
|
|
191
195
|
graphRules: {
|
|
192
196
|
onCreate: onCreateRules,
|
package/src/runtime/index.ts
CHANGED
|
@@ -93,7 +93,11 @@ function processEventNotifications(event: {
|
|
|
93
93
|
for (const [subscribableName, subs] of subscriptionsByName.entries()) {
|
|
94
94
|
for (const sub of subs) {
|
|
95
95
|
const paramValues = Object.values(sub.params);
|
|
96
|
-
|
|
96
|
+
// For subscribables with no params, the key is just the name
|
|
97
|
+
// For subscribables with params, the key is name:param1:param2:...
|
|
98
|
+
const subKey = paramValues.length > 0
|
|
99
|
+
? `${subscribableName}:${paramValues.join(':')}`
|
|
100
|
+
: subscribableName;
|
|
97
101
|
|
|
98
102
|
if (affected_keys.includes(subKey)) {
|
|
99
103
|
// Send entity broadcast (not subscription:event) so global dispatcher can route it
|
package/src/shared/ir.ts
CHANGED
|
@@ -130,6 +130,7 @@ export interface EntityIR {
|
|
|
130
130
|
delete: string[];
|
|
131
131
|
};
|
|
132
132
|
relationships: Record<string, RelationshipIR>;
|
|
133
|
+
includes: Record<string, IncludeIR>; // FK expansions (e.g., author: users)
|
|
133
134
|
manyToMany: Record<string, ManyToManyIR>;
|
|
134
135
|
graphRules: {
|
|
135
136
|
onCreate: GraphRuleIR[];
|