dzql 0.6.18 → 0.6.20

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.6.18",
3
+ "version": "0.6.20",
4
4
  "description": "Database-first real-time framework with TypeScript support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -583,6 +583,31 @@ export function generateGetFunction(name: string, entityIR: EntityIR): string {
583
583
  // Build SELECT expression excluding hidden fields
584
584
  const selectExpr = buildVisibleJsonb(name, entityIR.columns, hidden);
585
585
 
586
+ // FK expansion for GET (only direct FKs where {key}_id column exists)
587
+ // Reverse FK expansion (one-to-many) should be handled by subscribables for complex queries
588
+ const includes: Record<string, IncludeIR> = entityIR.includes || {};
589
+ const includeKeys = Object.keys(includes);
590
+ const fkExpansion = includeKeys.map(key => {
591
+ const config: IncludeIR = includes[key];
592
+ const targetEntity = config.entity;
593
+ const fkField = `${key}_id`; // Convention: author -> author_id
594
+
595
+ // Only expand direct FKs (key_id column exists on this entity)
596
+ const hasFkColumn = entityIR.columns.some((c: ColumnInfo) => c.name === fkField);
597
+
598
+ if (hasFkColumn) {
599
+ // Direct FK: single object expansion (e.g., author_id -> author object)
600
+ return `
601
+ -- FK: Add ${key} to result (from ${fkField})
602
+ IF (v_result->>'${fkField}') IS NOT NULL THEN
603
+ v_result := v_result || jsonb_build_object('${key}',
604
+ (SELECT to_jsonb(t.*) FROM ${targetEntity} t WHERE t.id = (v_result->>'${fkField}')::int));
605
+ END IF;`;
606
+ }
607
+ // Skip reverse FKs - use subscribables for complex document graphs
608
+ return '';
609
+ }).filter(s => s).join('\n');
610
+
586
611
  // M2M expansion for GET
587
612
  const m2m: Record<string, ManyToManyIR> = entityIR.manyToMany || {};
588
613
  const m2mKeys = Object.keys(m2m);
@@ -625,6 +650,7 @@ BEGIN
625
650
  IF v_result IS NULL THEN
626
651
  RETURN NULL;
627
652
  END IF;
653
+ ${fkExpansion}
628
654
  ${m2mExpansion}
629
655
 
630
656
  RETURN v_result;
@@ -133,14 +133,18 @@ export type FilterValue<T> = T | FilterOperators<T>;
133
133
 
134
134
  // Add includes as optional fields
135
135
  if (sub.includes) {
136
+ const rootEntityIR = entities[rootEntity];
137
+ const rootColumns = new Set(rootEntityIR.columns.map(c => c.name));
138
+
136
139
  for (const [includeKey, includeIR] of Object.entries(sub.includes)) {
137
140
  const includeEntity = includeIR.entity;
138
141
  const includeEntityPascal = toPascalCase(includeEntity);
139
142
 
140
- // Determine if it's an array (one-to-many) or single (many-to-one)
141
- // For now, assume arrays unless the include key matches a FK pattern
142
- const isArray = !includeKey.endsWith('_id') && includeKey !== rootEntity;
143
- const includeType = isArray ? `${includeEntityPascal}[]` : includeEntityPascal;
143
+ // Determine if it's many-to-one (singular) or one-to-many (array)
144
+ // If root entity has a column `{includeKey}_id`, it's many-to-one
145
+ const fkColumn = `${includeKey}_id`;
146
+ const isManyToOne = rootColumns.has(fkColumn);
147
+ const includeType = isManyToOne ? includeEntityPascal : `${includeEntityPascal}[]`;
144
148
 
145
149
  output += ` ${includeKey}?: ${includeType};\n`;
146
150
  }