linkgress-orm 0.1.1 → 0.1.4
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/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +7 -5
- package/dist/entity/db-context.js.map +1 -1
- package/dist/migration/db-schema-manager.d.ts +5 -0
- package/dist/migration/db-schema-manager.d.ts.map +1 -1
- package/dist/migration/db-schema-manager.js +70 -2
- package/dist/migration/db-schema-manager.js.map +1 -1
- package/dist/query/collection-strategy.interface.d.ts +38 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/cte-builder.d.ts +15 -1
- package/dist/query/cte-builder.d.ts.map +1 -1
- package/dist/query/cte-builder.js +121 -11
- package/dist/query/cte-builder.js.map +1 -1
- package/dist/query/grouped-query.d.ts +13 -1
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +147 -4
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/query-builder.d.ts +36 -1
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +359 -42
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.d.ts +4 -0
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +103 -12
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts +4 -0
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +61 -3
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/package.json +1 -1
|
@@ -121,7 +121,7 @@ const NUMERIC_REGEX = /^-?\d+(\.\d+)?$/;
|
|
|
121
121
|
* Query builder for a table
|
|
122
122
|
*/
|
|
123
123
|
class QueryBuilder {
|
|
124
|
-
constructor(schema, client, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, collectionStrategy) {
|
|
124
|
+
constructor(schema, client, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, collectionStrategy, schemaRegistry) {
|
|
125
125
|
this.orderByFields = [];
|
|
126
126
|
this.manualJoins = [];
|
|
127
127
|
this.joinCounter = 0;
|
|
@@ -135,6 +135,7 @@ class QueryBuilder {
|
|
|
135
135
|
this.manualJoins = manualJoins || [];
|
|
136
136
|
this.joinCounter = joinCounter || 0;
|
|
137
137
|
this.collectionStrategy = collectionStrategy;
|
|
138
|
+
this.schemaRegistry = schemaRegistry;
|
|
138
139
|
}
|
|
139
140
|
/**
|
|
140
141
|
* Get qualified table name with schema prefix if specified
|
|
@@ -148,7 +149,7 @@ class QueryBuilder {
|
|
|
148
149
|
*/
|
|
149
150
|
select(selector) {
|
|
150
151
|
return new SelectQueryBuilder(this.schema, this.client, selector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, // isDistinct defaults to false
|
|
151
|
-
|
|
152
|
+
this.schemaRegistry, // Pass schema registry for nested navigation resolution
|
|
152
153
|
[], // ctes - start with empty array
|
|
153
154
|
this.collectionStrategy);
|
|
154
155
|
}
|
|
@@ -171,7 +172,8 @@ class QueryBuilder {
|
|
|
171
172
|
* Add CTEs (Common Table Expressions) to the query
|
|
172
173
|
*/
|
|
173
174
|
with(...ctes) {
|
|
174
|
-
return new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false,
|
|
175
|
+
return new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, this.schemaRegistry, // Pass schema registry for nested navigation resolution
|
|
176
|
+
ctes, this.collectionStrategy);
|
|
175
177
|
}
|
|
176
178
|
/**
|
|
177
179
|
* Create mock row for analysis
|
|
@@ -200,12 +202,16 @@ class QueryBuilder {
|
|
|
200
202
|
const relationEntries = getRelationEntriesForSchema(this.schema);
|
|
201
203
|
// Add relations (both collections and single references)
|
|
202
204
|
for (const [relName, relConfig] of relationEntries) {
|
|
203
|
-
// Performance: Use cached target schema
|
|
204
|
-
|
|
205
|
+
// Performance: Use cached target schema, but prefer registry lookup for full relations
|
|
206
|
+
let targetSchema = this.schemaRegistry?.get(relConfig.targetTable);
|
|
207
|
+
if (!targetSchema) {
|
|
208
|
+
targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
|
|
209
|
+
}
|
|
205
210
|
if (relConfig.type === 'many') {
|
|
206
211
|
Object.defineProperty(mock, relName, {
|
|
207
212
|
get: () => {
|
|
208
|
-
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema
|
|
213
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
|
|
214
|
+
);
|
|
209
215
|
},
|
|
210
216
|
enumerable: true,
|
|
211
217
|
configurable: true,
|
|
@@ -215,7 +221,8 @@ class QueryBuilder {
|
|
|
215
221
|
// Single reference navigation (many-to-one, one-to-one)
|
|
216
222
|
Object.defineProperty(mock, relName, {
|
|
217
223
|
get: () => {
|
|
218
|
-
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema
|
|
224
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
|
|
225
|
+
);
|
|
219
226
|
return refBuilder.createMockTargetRow();
|
|
220
227
|
},
|
|
221
228
|
enumerable: true,
|
|
@@ -806,7 +813,7 @@ class SelectQueryBuilder {
|
|
|
806
813
|
get(target, prop) {
|
|
807
814
|
if (typeof prop === 'symbol')
|
|
808
815
|
return undefined;
|
|
809
|
-
// If we have selection metadata, check if this property has a mapper
|
|
816
|
+
// If we have selection metadata, check if this property has a mapper or is an aggregation array
|
|
810
817
|
if (cte.selectionMetadata && prop in cte.selectionMetadata) {
|
|
811
818
|
const value = cte.selectionMetadata[prop];
|
|
812
819
|
// If it's a SqlFragment with a mapper, preserve it
|
|
@@ -819,6 +826,16 @@ class SelectQueryBuilder {
|
|
|
819
826
|
getMapper: () => value.getMapper(),
|
|
820
827
|
};
|
|
821
828
|
}
|
|
829
|
+
// If it's a CTE aggregation array marker, preserve it with inner metadata
|
|
830
|
+
if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
|
|
831
|
+
return {
|
|
832
|
+
__fieldName: prop,
|
|
833
|
+
__dbColumnName: prop,
|
|
834
|
+
__tableAlias: cte.name,
|
|
835
|
+
__isAggregationArray: true,
|
|
836
|
+
__innerSelectionMetadata: value.__innerSelectionMetadata,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
822
839
|
}
|
|
823
840
|
// Return a regular FieldRef for any property accessed
|
|
824
841
|
return {
|
|
@@ -1462,31 +1479,117 @@ class SelectQueryBuilder {
|
|
|
1462
1479
|
}
|
|
1463
1480
|
/**
|
|
1464
1481
|
* Detect navigation property references in selection and add necessary JOINs
|
|
1482
|
+
* Supports multi-level navigation like task.level.createdBy
|
|
1465
1483
|
*/
|
|
1466
1484
|
detectAndAddJoinsFromSelection(selection, joins) {
|
|
1467
1485
|
if (!selection || typeof selection !== 'object') {
|
|
1468
1486
|
return;
|
|
1469
1487
|
}
|
|
1470
|
-
|
|
1488
|
+
// First pass: collect all table aliases
|
|
1489
|
+
const allTableAliases = new Set();
|
|
1490
|
+
this.collectTableAliasesFromSelection(selection, allTableAliases);
|
|
1491
|
+
// Second pass: resolve all joins through the schema graph
|
|
1492
|
+
this.resolveJoinsForTableAliases(allTableAliases, joins);
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Collect all table aliases from a selection
|
|
1496
|
+
*/
|
|
1497
|
+
collectTableAliasesFromSelection(selection, allTableAliases) {
|
|
1498
|
+
if (!selection || typeof selection !== 'object') {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
for (const [_key, value] of Object.entries(selection)) {
|
|
1471
1502
|
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
1472
|
-
// This is a FieldRef with a table alias
|
|
1473
|
-
|
|
1503
|
+
// This is a FieldRef with a table alias
|
|
1504
|
+
const tableAlias = value.__tableAlias;
|
|
1505
|
+
if (tableAlias && tableAlias !== this.schema.name) {
|
|
1506
|
+
allTableAliases.add(tableAlias);
|
|
1507
|
+
}
|
|
1474
1508
|
}
|
|
1475
1509
|
else if (value instanceof conditions_1.SqlFragment) {
|
|
1476
|
-
// SqlFragment may contain navigation property references
|
|
1510
|
+
// SqlFragment may contain navigation property references
|
|
1477
1511
|
const fieldRefs = value.getFieldRefs();
|
|
1478
1512
|
for (const fieldRef of fieldRefs) {
|
|
1479
|
-
|
|
1513
|
+
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
1514
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
1515
|
+
if (tableAlias && tableAlias !== this.schema.name) {
|
|
1516
|
+
allTableAliases.add(tableAlias);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1480
1519
|
}
|
|
1481
1520
|
}
|
|
1482
|
-
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1521
|
+
else if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof CollectionQueryBuilder)) {
|
|
1483
1522
|
// Recursively check nested objects
|
|
1484
|
-
this.
|
|
1523
|
+
this.collectTableAliasesFromSelection(value, allTableAliases);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Resolve all navigation joins by finding the correct path through the schema graph
|
|
1529
|
+
* This handles multi-level navigation like task.level.createdBy
|
|
1530
|
+
*/
|
|
1531
|
+
resolveJoinsForTableAliases(allTableAliases, joins) {
|
|
1532
|
+
if (allTableAliases.size === 0) {
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
// Keep resolving until we've resolved all aliases or can't make progress
|
|
1536
|
+
const resolved = new Set();
|
|
1537
|
+
let maxIterations = allTableAliases.size * 3; // Prevent infinite loops
|
|
1538
|
+
while (resolved.size < allTableAliases.size && maxIterations-- > 0) {
|
|
1539
|
+
// Build a map of already joined schemas for path resolution
|
|
1540
|
+
const joinedSchemas = new Map();
|
|
1541
|
+
joinedSchemas.set(this.schema.name, this.schema);
|
|
1542
|
+
for (const join of joins) {
|
|
1543
|
+
let schema;
|
|
1544
|
+
if (this.schemaRegistry) {
|
|
1545
|
+
schema = this.schemaRegistry.get(join.targetTable);
|
|
1546
|
+
}
|
|
1547
|
+
if (schema) {
|
|
1548
|
+
joinedSchemas.set(join.alias, schema);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
// Try to resolve each unresolved alias
|
|
1552
|
+
for (const alias of allTableAliases) {
|
|
1553
|
+
if (resolved.has(alias) || joins.some(j => j.alias === alias)) {
|
|
1554
|
+
resolved.add(alias);
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
// Look for this alias in any of the already joined schemas
|
|
1558
|
+
for (const [sourceAlias, schema] of joinedSchemas) {
|
|
1559
|
+
if (schema.relations && schema.relations[alias]) {
|
|
1560
|
+
const relation = schema.relations[alias];
|
|
1561
|
+
if (relation.type === 'one') {
|
|
1562
|
+
// Get target schema
|
|
1563
|
+
let targetSchema;
|
|
1564
|
+
let targetSchemaName;
|
|
1565
|
+
if (this.schemaRegistry) {
|
|
1566
|
+
targetSchema = this.schemaRegistry.get(relation.targetTable);
|
|
1567
|
+
targetSchemaName = targetSchema?.schema;
|
|
1568
|
+
}
|
|
1569
|
+
if (!targetSchema && relation.targetTableBuilder) {
|
|
1570
|
+
targetSchema = relation.targetTableBuilder.build();
|
|
1571
|
+
targetSchemaName = targetSchema?.schema;
|
|
1572
|
+
}
|
|
1573
|
+
joins.push({
|
|
1574
|
+
alias,
|
|
1575
|
+
targetTable: relation.targetTable,
|
|
1576
|
+
targetSchema: targetSchemaName,
|
|
1577
|
+
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
1578
|
+
matches: relation.matches || ['id'],
|
|
1579
|
+
isMandatory: relation.isMandatory ?? false,
|
|
1580
|
+
sourceAlias, // Track where this join comes from
|
|
1581
|
+
});
|
|
1582
|
+
resolved.add(alias);
|
|
1583
|
+
break;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1485
1587
|
}
|
|
1486
1588
|
}
|
|
1487
1589
|
}
|
|
1488
1590
|
/**
|
|
1489
1591
|
* Add a JOIN for a FieldRef if it references a related table
|
|
1592
|
+
* @deprecated Use detectAndAddJoinsFromSelection with multi-level resolution instead
|
|
1490
1593
|
*/
|
|
1491
1594
|
addJoinForFieldRef(fieldRef, joins) {
|
|
1492
1595
|
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef) || !('__dbColumnName' in fieldRef)) {
|
|
@@ -1522,35 +1625,19 @@ class SelectQueryBuilder {
|
|
|
1522
1625
|
if (!condition) {
|
|
1523
1626
|
return;
|
|
1524
1627
|
}
|
|
1525
|
-
//
|
|
1628
|
+
// Collect all table aliases from the condition
|
|
1629
|
+
const allTableAliases = new Set();
|
|
1526
1630
|
const fieldRefs = condition.getFieldRefs();
|
|
1527
1631
|
for (const fieldRef of fieldRefs) {
|
|
1528
1632
|
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
1529
1633
|
const tableAlias = fieldRef.__tableAlias;
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
// Find the relation config for this navigation
|
|
1533
|
-
const relation = this.schema.relations[tableAlias];
|
|
1534
|
-
if (relation && relation.type === 'one') {
|
|
1535
|
-
// Get target schema from targetTableBuilder if available
|
|
1536
|
-
let targetSchema;
|
|
1537
|
-
if (relation.targetTableBuilder) {
|
|
1538
|
-
const targetTableSchema = relation.targetTableBuilder.build();
|
|
1539
|
-
targetSchema = targetTableSchema.schema;
|
|
1540
|
-
}
|
|
1541
|
-
// Add a JOIN for this reference
|
|
1542
|
-
joins.push({
|
|
1543
|
-
alias: tableAlias,
|
|
1544
|
-
targetTable: relation.targetTable,
|
|
1545
|
-
targetSchema,
|
|
1546
|
-
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
1547
|
-
matches: relation.matches || [],
|
|
1548
|
-
isMandatory: relation.isMandatory ?? false,
|
|
1549
|
-
});
|
|
1550
|
-
}
|
|
1634
|
+
if (tableAlias !== this.schema.name) {
|
|
1635
|
+
allTableAliases.add(tableAlias);
|
|
1551
1636
|
}
|
|
1552
1637
|
}
|
|
1553
1638
|
}
|
|
1639
|
+
// Resolve all joins through the schema graph
|
|
1640
|
+
this.resolveJoinsForTableAliases(allTableAliases, joins);
|
|
1554
1641
|
}
|
|
1555
1642
|
/**
|
|
1556
1643
|
* Build SQL query
|
|
@@ -1632,6 +1719,7 @@ class SelectQueryBuilder {
|
|
|
1632
1719
|
if ('__tableAlias' in value && value.__tableAlias && typeof value.__tableAlias === 'string') {
|
|
1633
1720
|
// This is a field from a joined table
|
|
1634
1721
|
const tableAlias = value.__tableAlias;
|
|
1722
|
+
const columnName = value.__dbColumnName;
|
|
1635
1723
|
// Find the relation config for this navigation
|
|
1636
1724
|
const relConfig = this.schema.relations[tableAlias];
|
|
1637
1725
|
if (relConfig) {
|
|
@@ -1653,7 +1741,15 @@ class SelectQueryBuilder {
|
|
|
1653
1741
|
});
|
|
1654
1742
|
}
|
|
1655
1743
|
}
|
|
1656
|
-
|
|
1744
|
+
// Check if this is a CTE aggregation column that needs COALESCE
|
|
1745
|
+
const cteJoin = this.manualJoins.find(j => j.cte && j.cte.name === tableAlias);
|
|
1746
|
+
if (cteJoin && cteJoin.cte && cteJoin.cte.isAggregationColumn(columnName)) {
|
|
1747
|
+
// CTE aggregation column - wrap with COALESCE to return empty array instead of null
|
|
1748
|
+
selectParts.push(`COALESCE("${tableAlias}"."${columnName}", '[]'::jsonb) as "${key}"`);
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
selectParts.push(`"${tableAlias}"."${columnName}" as "${key}"`);
|
|
1752
|
+
}
|
|
1657
1753
|
}
|
|
1658
1754
|
else {
|
|
1659
1755
|
// Regular field from the main table
|
|
@@ -1912,11 +2008,14 @@ class SelectQueryBuilder {
|
|
|
1912
2008
|
for (const join of joins) {
|
|
1913
2009
|
const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1914
2010
|
// Build ON clause for the join
|
|
2011
|
+
// For multi-level navigation, use the sourceAlias (the intermediate table)
|
|
2012
|
+
// For direct navigation, use the main table name
|
|
2013
|
+
const sourceTable = join.sourceAlias || this.schema.name;
|
|
1915
2014
|
const onConditions = [];
|
|
1916
2015
|
for (let i = 0; i < join.foreignKeys.length; i++) {
|
|
1917
2016
|
const fk = join.foreignKeys[i];
|
|
1918
2017
|
const match = join.matches[i];
|
|
1919
|
-
onConditions.push(`"${
|
|
2018
|
+
onConditions.push(`"${sourceTable}"."${fk}" = "${join.alias}"."${match}"`);
|
|
1920
2019
|
}
|
|
1921
2020
|
// Use schema-qualified table name if schema is specified
|
|
1922
2021
|
const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
|
|
@@ -2048,6 +2147,17 @@ class SelectQueryBuilder {
|
|
|
2048
2147
|
}
|
|
2049
2148
|
}
|
|
2050
2149
|
}
|
|
2150
|
+
else if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
|
|
2151
|
+
// CTE withAggregation array - apply mappers to items inside
|
|
2152
|
+
const collectionItems = row[key] || [];
|
|
2153
|
+
const innerMetadata = value.__innerSelectionMetadata;
|
|
2154
|
+
if (innerMetadata && !disableMappers) {
|
|
2155
|
+
result[key] = this.transformCteAggregationItems(collectionItems, innerMetadata);
|
|
2156
|
+
}
|
|
2157
|
+
else {
|
|
2158
|
+
result[key] = collectionItems;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2051
2161
|
else if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
2052
2162
|
// SqlFragment with custom mapper (check this BEFORE FieldRef to handle subquery/CTE fields with mappers)
|
|
2053
2163
|
const rawValue = row[key];
|
|
@@ -2160,6 +2270,54 @@ class SelectQueryBuilder {
|
|
|
2160
2270
|
return transformedItem;
|
|
2161
2271
|
});
|
|
2162
2272
|
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Transform CTE aggregation items applying fromDriver mappers from selection metadata
|
|
2275
|
+
*/
|
|
2276
|
+
transformCteAggregationItems(items, selectionMetadata) {
|
|
2277
|
+
if (!items || items.length === 0) {
|
|
2278
|
+
return [];
|
|
2279
|
+
}
|
|
2280
|
+
// Build mapper cache from selection metadata
|
|
2281
|
+
const mapperCache = {};
|
|
2282
|
+
for (const [key, value] of Object.entries(selectionMetadata)) {
|
|
2283
|
+
// Check if value has getMapper (SqlFragment or field with mapper)
|
|
2284
|
+
if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
2285
|
+
let mapper = value.getMapper();
|
|
2286
|
+
// If mapper is a CustomTypeBuilder, get the actual type
|
|
2287
|
+
if (mapper && typeof mapper.getType === 'function') {
|
|
2288
|
+
mapper = mapper.getType();
|
|
2289
|
+
}
|
|
2290
|
+
if (mapper && typeof mapper.fromDriver === 'function') {
|
|
2291
|
+
mapperCache[key] = mapper;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
// Check if it's a FieldRef with schema column mapper
|
|
2295
|
+
else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
|
|
2296
|
+
const fieldName = value.__fieldName;
|
|
2297
|
+
const column = this.schema.columns[fieldName];
|
|
2298
|
+
if (column) {
|
|
2299
|
+
const config = column.build();
|
|
2300
|
+
if (config.mapper && typeof config.mapper.fromDriver === 'function') {
|
|
2301
|
+
mapperCache[key] = config.mapper;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
// Transform items
|
|
2307
|
+
return items.map(item => {
|
|
2308
|
+
const transformedItem = {};
|
|
2309
|
+
for (const [key, value] of Object.entries(item)) {
|
|
2310
|
+
const mapper = mapperCache[key];
|
|
2311
|
+
if (mapper && value !== null && value !== undefined) {
|
|
2312
|
+
transformedItem[key] = mapper.fromDriver(value);
|
|
2313
|
+
}
|
|
2314
|
+
else {
|
|
2315
|
+
transformedItem[key] = value;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return transformedItem;
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2163
2321
|
/**
|
|
2164
2322
|
* Build aggregation query (MIN, MAX, SUM)
|
|
2165
2323
|
*/
|
|
@@ -2493,7 +2651,8 @@ class CollectionQueryBuilder {
|
|
|
2493
2651
|
* Select specific fields from collection items
|
|
2494
2652
|
*/
|
|
2495
2653
|
select(selector) {
|
|
2496
|
-
const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema
|
|
2654
|
+
const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
|
|
2655
|
+
);
|
|
2497
2656
|
newBuilder.selector = selector;
|
|
2498
2657
|
newBuilder.whereCond = this.whereCond;
|
|
2499
2658
|
newBuilder.limitValue = this.limitValue;
|
|
@@ -2726,6 +2885,151 @@ class CollectionQueryBuilder {
|
|
|
2726
2885
|
getFlattenResultType() {
|
|
2727
2886
|
return this.flattenResultType;
|
|
2728
2887
|
}
|
|
2888
|
+
/**
|
|
2889
|
+
* Detect navigation property references in the selected fields and add necessary JOINs
|
|
2890
|
+
* This supports multi-level navigation like p.task.level.createdBy.username
|
|
2891
|
+
*/
|
|
2892
|
+
detectNavigationJoins(selection, joins, currentSourceAlias, currentSchema) {
|
|
2893
|
+
if (!selection || typeof selection !== 'object') {
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
// Collect all table aliases referenced in the selection
|
|
2897
|
+
const allTableAliases = new Set();
|
|
2898
|
+
// Helper to collect from a single selection
|
|
2899
|
+
const collectFromSelection = (sel) => {
|
|
2900
|
+
if (!sel || typeof sel !== 'object') {
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
// Handle single FieldRef
|
|
2904
|
+
if ('__tableAlias' in sel && '__dbColumnName' in sel) {
|
|
2905
|
+
this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
// Handle object with multiple fields
|
|
2909
|
+
for (const [_key, value] of Object.entries(sel)) {
|
|
2910
|
+
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
2911
|
+
// This is a FieldRef with a table alias
|
|
2912
|
+
this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
2913
|
+
}
|
|
2914
|
+
else if (value instanceof conditions_1.SqlFragment) {
|
|
2915
|
+
// SqlFragment may contain navigation property references
|
|
2916
|
+
const fieldRefs = value.getFieldRefs();
|
|
2917
|
+
for (const fieldRef of fieldRefs) {
|
|
2918
|
+
this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
else if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof CollectionQueryBuilder)) {
|
|
2922
|
+
// Recursively check nested objects
|
|
2923
|
+
collectFromSelection(value);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
};
|
|
2927
|
+
// First pass: collect all table aliases
|
|
2928
|
+
collectFromSelection(selection);
|
|
2929
|
+
// Second pass: resolve all navigation joins by finding the correct path through schemas
|
|
2930
|
+
if (allTableAliases.size > 0) {
|
|
2931
|
+
this.resolveNavigationJoins(allTableAliases, joins, currentSchema);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Add a navigation JOIN for a FieldRef if it references a related table
|
|
2936
|
+
* Handles multi-level navigation by recursively resolving the join chain
|
|
2937
|
+
*/
|
|
2938
|
+
addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases) {
|
|
2939
|
+
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef)) {
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
2943
|
+
// If this references the target table directly, no join needed
|
|
2944
|
+
if (!tableAlias || tableAlias === this.targetTable) {
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
// Collect this table alias for later resolution
|
|
2948
|
+
allTableAliases.add(tableAlias);
|
|
2949
|
+
// Check if we already have this join
|
|
2950
|
+
if (joins.some(j => j.alias === tableAlias)) {
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
// Find the relation in the current schema
|
|
2954
|
+
const relation = sourceSchema.relations?.[tableAlias];
|
|
2955
|
+
if (relation && relation.type === 'one') {
|
|
2956
|
+
this.addNavigationJoin(tableAlias, relation, joins, sourceAlias);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* Add a navigation join and return the target schema
|
|
2961
|
+
*/
|
|
2962
|
+
addNavigationJoin(alias, relation, joins, sourceAlias) {
|
|
2963
|
+
// Check if already added
|
|
2964
|
+
if (joins.some(j => j.alias === alias)) {
|
|
2965
|
+
return undefined;
|
|
2966
|
+
}
|
|
2967
|
+
// Get the target table schema
|
|
2968
|
+
let targetSchema;
|
|
2969
|
+
let targetSchemaName;
|
|
2970
|
+
if (this.schemaRegistry) {
|
|
2971
|
+
targetSchema = this.schemaRegistry.get(relation.targetTable);
|
|
2972
|
+
targetSchemaName = targetSchema?.schema;
|
|
2973
|
+
}
|
|
2974
|
+
if (!targetSchema && relation.targetTableBuilder) {
|
|
2975
|
+
targetSchema = relation.targetTableBuilder.build();
|
|
2976
|
+
targetSchemaName = targetSchema?.schema;
|
|
2977
|
+
}
|
|
2978
|
+
// Build the join info
|
|
2979
|
+
const foreignKeys = relation.foreignKeys || [relation.foreignKey || ''];
|
|
2980
|
+
const matches = relation.matches || ['id']; // Default to 'id' as the PK
|
|
2981
|
+
joins.push({
|
|
2982
|
+
alias,
|
|
2983
|
+
targetTable: relation.targetTable,
|
|
2984
|
+
targetSchema: targetSchemaName,
|
|
2985
|
+
foreignKeys,
|
|
2986
|
+
matches,
|
|
2987
|
+
isMandatory: relation.isMandatory ?? false,
|
|
2988
|
+
sourceAlias,
|
|
2989
|
+
});
|
|
2990
|
+
return targetSchema;
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Resolve all navigation joins by finding the correct path through the schema graph
|
|
2994
|
+
* This handles multi-level navigation like task.level.createdBy
|
|
2995
|
+
*/
|
|
2996
|
+
resolveNavigationJoins(allTableAliases, joins, startSchema) {
|
|
2997
|
+
// Keep resolving until we've resolved all aliases or can't make progress
|
|
2998
|
+
let resolved = new Set();
|
|
2999
|
+
let maxIterations = allTableAliases.size * 2; // Prevent infinite loops
|
|
3000
|
+
while (resolved.size < allTableAliases.size && maxIterations-- > 0) {
|
|
3001
|
+
// Build a map of already joined schemas for path resolution
|
|
3002
|
+
const joinedSchemas = new Map();
|
|
3003
|
+
joinedSchemas.set(this.targetTable, startSchema);
|
|
3004
|
+
for (const join of joins) {
|
|
3005
|
+
let schema;
|
|
3006
|
+
if (this.schemaRegistry) {
|
|
3007
|
+
schema = this.schemaRegistry.get(join.targetTable);
|
|
3008
|
+
}
|
|
3009
|
+
if (schema) {
|
|
3010
|
+
joinedSchemas.set(join.alias, schema);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
// Try to resolve each unresolved alias
|
|
3014
|
+
for (const alias of allTableAliases) {
|
|
3015
|
+
if (resolved.has(alias) || joins.some(j => j.alias === alias)) {
|
|
3016
|
+
resolved.add(alias);
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
// Look for this alias in any of the already joined schemas
|
|
3020
|
+
for (const [schemaAlias, schema] of joinedSchemas) {
|
|
3021
|
+
if (schema.relations && schema.relations[alias]) {
|
|
3022
|
+
const relation = schema.relations[alias];
|
|
3023
|
+
if (relation.type === 'one') {
|
|
3024
|
+
this.addNavigationJoin(alias, relation, joins, schemaAlias);
|
|
3025
|
+
resolved.add(alias);
|
|
3026
|
+
break;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
2729
3033
|
/**
|
|
2730
3034
|
* Build CTE for this collection query
|
|
2731
3035
|
* Now delegates to collection strategy pattern
|
|
@@ -2760,8 +3064,13 @@ class CollectionQueryBuilder {
|
|
|
2760
3064
|
return { alias, expression: fragmentSql };
|
|
2761
3065
|
}
|
|
2762
3066
|
else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
2763
|
-
// FieldRef object - use database column name
|
|
3067
|
+
// FieldRef object - use database column name with optional table alias
|
|
2764
3068
|
const dbColumnName = field.__dbColumnName;
|
|
3069
|
+
const tableAlias = field.__tableAlias;
|
|
3070
|
+
// If tableAlias differs from the target table, it's a navigation property reference
|
|
3071
|
+
if (tableAlias && tableAlias !== this.targetTable) {
|
|
3072
|
+
return { alias, expression: `"${tableAlias}"."${dbColumnName}"` };
|
|
3073
|
+
}
|
|
2765
3074
|
return { alias, expression: `"${dbColumnName}"` };
|
|
2766
3075
|
}
|
|
2767
3076
|
else if (typeof field === 'string') {
|
|
@@ -2884,7 +3193,14 @@ class CollectionQueryBuilder {
|
|
|
2884
3193
|
aggregationType = 'jsonb';
|
|
2885
3194
|
defaultValue = "'[]'::jsonb";
|
|
2886
3195
|
}
|
|
2887
|
-
// Step 5:
|
|
3196
|
+
// Step 5: Detect navigation joins from the selected fields
|
|
3197
|
+
const navigationJoins = [];
|
|
3198
|
+
if (this.selector && this.targetTableSchema) {
|
|
3199
|
+
const mockItem = this.createMockItem();
|
|
3200
|
+
const selectedFields = this.selector(mockItem);
|
|
3201
|
+
this.detectNavigationJoins(selectedFields, navigationJoins, this.targetTable, this.targetTableSchema);
|
|
3202
|
+
}
|
|
3203
|
+
// Step 6: Build CollectionAggregationConfig object
|
|
2888
3204
|
const config = {
|
|
2889
3205
|
relationName: this.relationName,
|
|
2890
3206
|
targetTable: this.targetTable,
|
|
@@ -2903,6 +3219,7 @@ class CollectionQueryBuilder {
|
|
|
2903
3219
|
arrayField,
|
|
2904
3220
|
defaultValue,
|
|
2905
3221
|
counter: context.cteCounter++,
|
|
3222
|
+
navigationJoins: navigationJoins.length > 0 ? navigationJoins : undefined,
|
|
2906
3223
|
};
|
|
2907
3224
|
// Step 6: Call the strategy
|
|
2908
3225
|
const result = strategy.buildAggregation(config, context, client);
|