linkgress-orm 0.1.18 → 0.1.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/dist/database/database-client.interface.d.ts +29 -0
- package/dist/database/database-client.interface.d.ts.map +1 -1
- package/dist/database/database-client.interface.js +45 -1
- package/dist/database/database-client.interface.js.map +1 -1
- package/dist/database/pg-client.d.ts +5 -0
- package/dist/database/pg-client.d.ts.map +1 -1
- package/dist/database/pg-client.js +27 -0
- package/dist/database/pg-client.js.map +1 -1
- package/dist/database/postgres-client.d.ts +6 -0
- package/dist/database/postgres-client.d.ts.map +1 -1
- package/dist/database/postgres-client.js +17 -0
- package/dist/database/postgres-client.js.map +1 -1
- package/dist/entity/db-column.d.ts +17 -0
- package/dist/entity/db-column.d.ts.map +1 -1
- package/dist/entity/db-column.js.map +1 -1
- package/dist/entity/db-context.d.ts +56 -6
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +180 -74
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/query/collection-strategy.interface.d.ts +12 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/query-builder.d.ts +15 -2
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +179 -48
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/sql-utils.d.ts +2 -0
- package/dist/query/sql-utils.d.ts.map +1 -1
- package/dist/query/sql-utils.js +24 -7
- package/dist/query/sql-utils.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +4 -3
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
- package/package.json +2 -3
|
@@ -247,24 +247,27 @@ class QueryBuilder {
|
|
|
247
247
|
targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
|
|
248
248
|
}
|
|
249
249
|
if (relConfig.type === 'many') {
|
|
250
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
250
251
|
Object.defineProperty(mock, relName, {
|
|
251
252
|
get: () => {
|
|
252
253
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
|
|
253
254
|
);
|
|
254
255
|
},
|
|
255
|
-
enumerable:
|
|
256
|
+
enumerable: false,
|
|
256
257
|
configurable: true,
|
|
257
258
|
});
|
|
258
259
|
}
|
|
259
260
|
else {
|
|
260
261
|
// Single reference navigation (many-to-one, one-to-one)
|
|
262
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
|
|
263
|
+
// with circular relations like User->Posts->User)
|
|
261
264
|
Object.defineProperty(mock, relName, {
|
|
262
265
|
get: () => {
|
|
263
266
|
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
|
|
264
267
|
);
|
|
265
268
|
return refBuilder.createMockTargetRow();
|
|
266
269
|
},
|
|
267
|
-
enumerable:
|
|
270
|
+
enumerable: false,
|
|
268
271
|
configurable: true,
|
|
269
272
|
});
|
|
270
273
|
}
|
|
@@ -406,22 +409,24 @@ class QueryBuilder {
|
|
|
406
409
|
const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
|
|
407
410
|
if (relConfig.type === 'many') {
|
|
408
411
|
// Collection navigation
|
|
412
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
409
413
|
Object.defineProperty(mock, relName, {
|
|
410
414
|
get: () => {
|
|
411
415
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
|
|
412
416
|
},
|
|
413
|
-
enumerable:
|
|
417
|
+
enumerable: false,
|
|
414
418
|
configurable: true,
|
|
415
419
|
});
|
|
416
420
|
}
|
|
417
421
|
else {
|
|
418
422
|
// Single reference navigation
|
|
423
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
419
424
|
Object.defineProperty(mock, relName, {
|
|
420
425
|
get: () => {
|
|
421
426
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
422
427
|
return refBuilder.createMockTargetRow();
|
|
423
428
|
},
|
|
424
|
-
enumerable:
|
|
429
|
+
enumerable: false,
|
|
425
430
|
configurable: true,
|
|
426
431
|
});
|
|
427
432
|
}
|
|
@@ -786,22 +791,24 @@ class SelectQueryBuilder {
|
|
|
786
791
|
const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
|
|
787
792
|
if (relConfig.type === 'many') {
|
|
788
793
|
// Collection navigation
|
|
794
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
789
795
|
Object.defineProperty(mock, relName, {
|
|
790
796
|
get: () => {
|
|
791
797
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
|
|
792
798
|
},
|
|
793
|
-
enumerable:
|
|
799
|
+
enumerable: false,
|
|
794
800
|
configurable: true,
|
|
795
801
|
});
|
|
796
802
|
}
|
|
797
803
|
else {
|
|
798
804
|
// Single reference navigation
|
|
805
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
799
806
|
Object.defineProperty(mock, relName, {
|
|
800
807
|
get: () => {
|
|
801
808
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
802
809
|
return refBuilder.createMockTargetRow();
|
|
803
810
|
},
|
|
804
|
-
enumerable:
|
|
811
|
+
enumerable: false,
|
|
805
812
|
configurable: true,
|
|
806
813
|
});
|
|
807
814
|
}
|
|
@@ -1473,8 +1480,10 @@ class SelectQueryBuilder {
|
|
|
1473
1480
|
}
|
|
1474
1481
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1475
1482
|
const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, 1);
|
|
1476
|
-
// Build RETURNING clause
|
|
1477
|
-
const returningClause =
|
|
1483
|
+
// Build RETURNING clause (not needed for count-only)
|
|
1484
|
+
const returningClause = returning !== 'count'
|
|
1485
|
+
? queryBuilder.buildDeleteReturningClause(returning)
|
|
1486
|
+
: undefined;
|
|
1478
1487
|
const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
|
|
1479
1488
|
let sql = `DELETE FROM ${qualifiedTableName} WHERE ${whereSql}`;
|
|
1480
1489
|
if (returningClause) {
|
|
@@ -1483,6 +1492,10 @@ class SelectQueryBuilder {
|
|
|
1483
1492
|
const result = queryBuilder.executor
|
|
1484
1493
|
? await queryBuilder.executor.query(sql, whereParams)
|
|
1485
1494
|
: await queryBuilder.client.query(sql, whereParams);
|
|
1495
|
+
// Return affected count
|
|
1496
|
+
if (returning === 'count') {
|
|
1497
|
+
return result.rowCount ?? 0;
|
|
1498
|
+
}
|
|
1486
1499
|
if (!returningClause) {
|
|
1487
1500
|
return undefined;
|
|
1488
1501
|
}
|
|
@@ -1492,6 +1505,13 @@ class SelectQueryBuilder {
|
|
|
1492
1505
|
then(onfulfilled, onrejected) {
|
|
1493
1506
|
return executeDelete(undefined).then(onfulfilled, onrejected);
|
|
1494
1507
|
},
|
|
1508
|
+
affectedCount() {
|
|
1509
|
+
return {
|
|
1510
|
+
then(onfulfilled, onrejected) {
|
|
1511
|
+
return executeDelete('count').then(onfulfilled, onrejected);
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
},
|
|
1495
1515
|
returning(selector) {
|
|
1496
1516
|
const returningConfig = selector ?? true;
|
|
1497
1517
|
return {
|
|
@@ -1550,8 +1570,10 @@ class SelectQueryBuilder {
|
|
|
1550
1570
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1551
1571
|
const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, paramIndex);
|
|
1552
1572
|
values.push(...whereParams);
|
|
1553
|
-
// Build RETURNING clause
|
|
1554
|
-
const returningClause =
|
|
1573
|
+
// Build RETURNING clause (not needed for count-only)
|
|
1574
|
+
const returningClause = returning !== 'count'
|
|
1575
|
+
? queryBuilder.buildDeleteReturningClause(returning)
|
|
1576
|
+
: undefined;
|
|
1555
1577
|
const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
|
|
1556
1578
|
let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')} WHERE ${whereSql}`;
|
|
1557
1579
|
if (returningClause) {
|
|
@@ -1560,6 +1582,10 @@ class SelectQueryBuilder {
|
|
|
1560
1582
|
const result = queryBuilder.executor
|
|
1561
1583
|
? await queryBuilder.executor.query(sql, values)
|
|
1562
1584
|
: await queryBuilder.client.query(sql, values);
|
|
1585
|
+
// Return affected count
|
|
1586
|
+
if (returning === 'count') {
|
|
1587
|
+
return result.rowCount ?? 0;
|
|
1588
|
+
}
|
|
1563
1589
|
if (!returningClause) {
|
|
1564
1590
|
return undefined;
|
|
1565
1591
|
}
|
|
@@ -1569,6 +1595,13 @@ class SelectQueryBuilder {
|
|
|
1569
1595
|
then(onfulfilled, onrejected) {
|
|
1570
1596
|
return executeUpdate(undefined).then(onfulfilled, onrejected);
|
|
1571
1597
|
},
|
|
1598
|
+
affectedCount() {
|
|
1599
|
+
return {
|
|
1600
|
+
then(onfulfilled, onrejected) {
|
|
1601
|
+
return executeUpdate('count').then(onfulfilled, onrejected);
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
},
|
|
1572
1605
|
returning(selector) {
|
|
1573
1606
|
const returningConfig = selector ?? true;
|
|
1574
1607
|
return {
|
|
@@ -1737,18 +1770,20 @@ class SelectQueryBuilder {
|
|
|
1737
1770
|
targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
|
|
1738
1771
|
}
|
|
1739
1772
|
if (relConfig.type === 'many') {
|
|
1773
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
1740
1774
|
Object.defineProperty(mock, relName, {
|
|
1741
1775
|
get: () => {
|
|
1742
1776
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, // Pass the target schema directly
|
|
1743
1777
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
1744
1778
|
);
|
|
1745
1779
|
},
|
|
1746
|
-
enumerable:
|
|
1780
|
+
enumerable: false,
|
|
1747
1781
|
configurable: true,
|
|
1748
1782
|
});
|
|
1749
1783
|
}
|
|
1750
1784
|
else {
|
|
1751
1785
|
// For single reference (many-to-one), create a ReferenceQueryBuilder
|
|
1786
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
1752
1787
|
Object.defineProperty(mock, relName, {
|
|
1753
1788
|
get: () => {
|
|
1754
1789
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
|
|
@@ -1757,7 +1792,7 @@ class SelectQueryBuilder {
|
|
|
1757
1792
|
// Return a mock object that exposes the target table's columns
|
|
1758
1793
|
return refBuilder.createMockTargetRow();
|
|
1759
1794
|
},
|
|
1760
|
-
enumerable:
|
|
1795
|
+
enumerable: false,
|
|
1761
1796
|
configurable: true,
|
|
1762
1797
|
});
|
|
1763
1798
|
}
|
|
@@ -2640,26 +2675,58 @@ class SelectQueryBuilder {
|
|
|
2640
2675
|
// Use pre-cached column metadata from target schema
|
|
2641
2676
|
// This avoids repeated column.build() calls for each item
|
|
2642
2677
|
const columnCache = targetSchema.columnMetadataCache;
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2678
|
+
// Build alias-to-field-info mapping from selected field configs
|
|
2679
|
+
// This allows us to find the correct mapper when:
|
|
2680
|
+
// 1. alias differs from property name (e.g., reservationExpiry: i.expiresAt)
|
|
2681
|
+
// 2. field comes from navigation (e.g., customerBirthdate: i.userEshop.birthdate)
|
|
2682
|
+
const selectedFieldConfigs = collectionBuilder.getSelectedFieldConfigs();
|
|
2683
|
+
const aliasToFieldInfo = new Map();
|
|
2684
|
+
if (selectedFieldConfigs) {
|
|
2685
|
+
for (const field of selectedFieldConfigs) {
|
|
2686
|
+
if (field.propertyName) {
|
|
2687
|
+
aliasToFieldInfo.set(field.alias, {
|
|
2688
|
+
propertyName: field.propertyName,
|
|
2689
|
+
sourceTable: field.sourceTable,
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
// Pre-build mapper cache for all fields (including navigation fields)
|
|
2695
|
+
// This avoids repeated schema lookups per item
|
|
2696
|
+
const mapperCache = new Map(); // alias -> mapper
|
|
2697
|
+
for (const [alias, fieldInfo] of aliasToFieldInfo) {
|
|
2698
|
+
let mapper = null;
|
|
2699
|
+
const schemaRegistry = collectionBuilder.getSchemaRegistry() || this.schemaRegistry;
|
|
2700
|
+
if (fieldInfo.sourceTable && schemaRegistry) {
|
|
2701
|
+
// Navigation field - look up mapper from the source table's schema
|
|
2702
|
+
const navSchema = schemaRegistry.get(fieldInfo.sourceTable);
|
|
2703
|
+
if (navSchema?.columnMetadataCache) {
|
|
2704
|
+
const cached = navSchema.columnMetadataCache.get(fieldInfo.propertyName);
|
|
2705
|
+
if (cached?.hasMapper) {
|
|
2706
|
+
mapper = cached.mapper;
|
|
2657
2707
|
}
|
|
2658
2708
|
}
|
|
2659
|
-
|
|
2660
|
-
|
|
2709
|
+
}
|
|
2710
|
+
else {
|
|
2711
|
+
// Regular field from target schema
|
|
2712
|
+
const cached = columnCache?.get(fieldInfo.propertyName);
|
|
2713
|
+
if (cached?.hasMapper) {
|
|
2714
|
+
mapper = cached.mapper;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (mapper) {
|
|
2718
|
+
mapperCache.set(alias, mapper);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
// Also add direct property matches from target schema (when no alias mapping)
|
|
2722
|
+
if (columnCache) {
|
|
2723
|
+
for (const [propertyName, cached] of columnCache) {
|
|
2724
|
+
if (!mapperCache.has(propertyName) && cached.hasMapper) {
|
|
2725
|
+
mapperCache.set(propertyName, cached.mapper);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2661
2728
|
}
|
|
2662
|
-
//
|
|
2729
|
+
// Transform items using pre-built mapper cache
|
|
2663
2730
|
const results = new Array(items.length);
|
|
2664
2731
|
let i = items.length;
|
|
2665
2732
|
while (i--) {
|
|
@@ -2667,9 +2734,9 @@ class SelectQueryBuilder {
|
|
|
2667
2734
|
const transformedItem = {};
|
|
2668
2735
|
for (const key in item) {
|
|
2669
2736
|
const value = item[key];
|
|
2670
|
-
const
|
|
2671
|
-
if (
|
|
2672
|
-
transformedItem[key] =
|
|
2737
|
+
const mapper = mapperCache.get(key);
|
|
2738
|
+
if (mapper) {
|
|
2739
|
+
transformedItem[key] = mapper.fromDriver(value);
|
|
2673
2740
|
}
|
|
2674
2741
|
else {
|
|
2675
2742
|
transformedItem[key] = value;
|
|
@@ -2929,13 +2996,15 @@ exports.SelectQueryBuilder = SelectQueryBuilder;
|
|
|
2929
2996
|
* Reference query builder for single navigation (many-to-one, one-to-one)
|
|
2930
2997
|
*/
|
|
2931
2998
|
class ReferenceQueryBuilder {
|
|
2932
|
-
constructor(relationName, targetTable, foreignKeys, matches, isMandatory, targetTableSchema, schemaRegistry) {
|
|
2999
|
+
constructor(relationName, targetTable, foreignKeys, matches, isMandatory, targetTableSchema, schemaRegistry, navigationPath, sourceAlias) {
|
|
2933
3000
|
this.relationName = relationName;
|
|
2934
3001
|
this.targetTable = targetTable;
|
|
2935
3002
|
this.foreignKeys = foreignKeys;
|
|
2936
3003
|
this.matches = matches;
|
|
2937
3004
|
this.isMandatory = isMandatory;
|
|
2938
3005
|
this.schemaRegistry = schemaRegistry;
|
|
3006
|
+
this.navigationPath = navigationPath || [];
|
|
3007
|
+
this.sourceAlias = sourceAlias || '';
|
|
2939
3008
|
// Prefer registry lookup (has full relations) over passed schema
|
|
2940
3009
|
if (this.schemaRegistry) {
|
|
2941
3010
|
this.targetTableSchema = this.schemaRegistry.get(targetTable);
|
|
@@ -3001,6 +3070,7 @@ class ReferenceQueryBuilder {
|
|
|
3001
3070
|
columnMappers[colName] = config.mapper;
|
|
3002
3071
|
}
|
|
3003
3072
|
}
|
|
3073
|
+
const sourceTable = this.targetTable; // Actual table name for schema lookup
|
|
3004
3074
|
for (const [colName, dbColumnName] of columnNameMap) {
|
|
3005
3075
|
const mapper = columnMappers[colName];
|
|
3006
3076
|
Object.defineProperty(mock, colName, {
|
|
@@ -3010,7 +3080,8 @@ class ReferenceQueryBuilder {
|
|
|
3010
3080
|
cached = fieldRefCache[colName] = {
|
|
3011
3081
|
__fieldName: colName,
|
|
3012
3082
|
__dbColumnName: dbColumnName,
|
|
3013
|
-
__tableAlias: tableAlias, //
|
|
3083
|
+
__tableAlias: tableAlias, // Alias for SQL generation
|
|
3084
|
+
__sourceTable: sourceTable, // Actual table name for mapper lookup
|
|
3014
3085
|
__mapper: mapper, // Include mapper for toDriver transformation in conditions
|
|
3015
3086
|
};
|
|
3016
3087
|
}
|
|
@@ -3020,6 +3091,23 @@ class ReferenceQueryBuilder {
|
|
|
3020
3091
|
configurable: true,
|
|
3021
3092
|
});
|
|
3022
3093
|
}
|
|
3094
|
+
// Build extended navigation path for nested collections
|
|
3095
|
+
// Only build navigation path if we have a sourceAlias (meaning we're inside a collection's selector)
|
|
3096
|
+
// If sourceAlias is empty, we're in the main query and references are joined in the FROM clause
|
|
3097
|
+
let extendedNavPath = [];
|
|
3098
|
+
if (this.sourceAlias) {
|
|
3099
|
+
// Build the current navigation step to include in path for nested collections
|
|
3100
|
+
// This represents the join from sourceAlias to this.relationName (this.targetTable)
|
|
3101
|
+
const currentNavStep = {
|
|
3102
|
+
alias: this.relationName,
|
|
3103
|
+
targetTable: this.targetTable,
|
|
3104
|
+
foreignKeys: this.foreignKeys,
|
|
3105
|
+
matches: this.matches.length > 0 ? this.matches : ['id'], // Default to 'id' if not specified
|
|
3106
|
+
isMandatory: this.isMandatory,
|
|
3107
|
+
sourceAlias: this.sourceAlias,
|
|
3108
|
+
};
|
|
3109
|
+
extendedNavPath = [...this.navigationPath, currentNavStep];
|
|
3110
|
+
}
|
|
3023
3111
|
// Add navigation properties (both collections and references)
|
|
3024
3112
|
if (this.targetTableSchema.relations) {
|
|
3025
3113
|
for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
|
|
@@ -3033,27 +3121,34 @@ class ReferenceQueryBuilder {
|
|
|
3033
3121
|
}
|
|
3034
3122
|
if (relConfig.type === 'many') {
|
|
3035
3123
|
// Collection navigation
|
|
3124
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
3036
3125
|
Object.defineProperty(mock, relName, {
|
|
3037
3126
|
get: () => {
|
|
3038
3127
|
const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
|
|
3039
|
-
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.
|
|
3040
|
-
|
|
3128
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.relationName, // Use alias (relationName) for correlation in lateral joins
|
|
3129
|
+
nestedTargetSchema, // Pass the target schema directly
|
|
3130
|
+
this.schemaRegistry, // Pass schema registry for nested resolution
|
|
3131
|
+
extendedNavPath // Pass navigation path for intermediate joins (empty if main query)
|
|
3041
3132
|
);
|
|
3042
3133
|
},
|
|
3043
|
-
enumerable:
|
|
3134
|
+
enumerable: false,
|
|
3044
3135
|
configurable: true,
|
|
3045
3136
|
});
|
|
3046
3137
|
}
|
|
3047
3138
|
else {
|
|
3048
3139
|
// Reference navigation
|
|
3140
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
|
|
3141
|
+
// with circular relations like User->Posts->User)
|
|
3049
3142
|
Object.defineProperty(mock, relName, {
|
|
3050
3143
|
get: () => {
|
|
3051
3144
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
|
|
3052
|
-
this.schemaRegistry // Pass schema registry for nested resolution
|
|
3145
|
+
this.schemaRegistry, // Pass schema registry for nested resolution
|
|
3146
|
+
extendedNavPath, // Pass navigation path for nested collections
|
|
3147
|
+
this.sourceAlias ? this.relationName : '' // Only set source if tracking path
|
|
3053
3148
|
);
|
|
3054
3149
|
return refBuilder.createMockTargetRow();
|
|
3055
3150
|
},
|
|
3056
|
-
enumerable:
|
|
3151
|
+
enumerable: false,
|
|
3057
3152
|
configurable: true,
|
|
3058
3153
|
});
|
|
3059
3154
|
}
|
|
@@ -3072,7 +3167,7 @@ exports.ReferenceQueryBuilder = ReferenceQueryBuilder;
|
|
|
3072
3167
|
* Collection query builder for nested queries
|
|
3073
3168
|
*/
|
|
3074
3169
|
class CollectionQueryBuilder {
|
|
3075
|
-
constructor(relationName, targetTable, foreignKey, sourceTable, targetTableSchema, schemaRegistry) {
|
|
3170
|
+
constructor(relationName, targetTable, foreignKey, sourceTable, targetTableSchema, schemaRegistry, navigationPath) {
|
|
3076
3171
|
this.orderByFields = [];
|
|
3077
3172
|
this.isMarkedAsList = false;
|
|
3078
3173
|
this.isDistinct = false;
|
|
@@ -3082,6 +3177,7 @@ class CollectionQueryBuilder {
|
|
|
3082
3177
|
this.foreignKey = foreignKey;
|
|
3083
3178
|
this.sourceTable = sourceTable;
|
|
3084
3179
|
this.schemaRegistry = schemaRegistry;
|
|
3180
|
+
this.navigationPath = navigationPath || [];
|
|
3085
3181
|
// Prefer registry lookup (has full relations) over passed schema
|
|
3086
3182
|
if (this.schemaRegistry) {
|
|
3087
3183
|
const registrySchema = this.schemaRegistry.get(targetTable);
|
|
@@ -3098,7 +3194,8 @@ class CollectionQueryBuilder {
|
|
|
3098
3194
|
* Select specific fields from collection items
|
|
3099
3195
|
*/
|
|
3100
3196
|
select(selector) {
|
|
3101
|
-
const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
|
|
3197
|
+
const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema, this.schemaRegistry, // Pass schema registry for nested navigation resolution
|
|
3198
|
+
this.navigationPath // Pass navigation path for intermediate joins
|
|
3102
3199
|
);
|
|
3103
3200
|
newBuilder.selector = selector;
|
|
3104
3201
|
newBuilder.whereCond = this.whereCond;
|
|
@@ -3148,7 +3245,8 @@ class CollectionQueryBuilder {
|
|
|
3148
3245
|
const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
3149
3246
|
// Performance: Lazy-cache FieldRef objects
|
|
3150
3247
|
const fieldRefCache = {};
|
|
3151
|
-
// Add columns
|
|
3248
|
+
// Add columns - include tableAlias for unambiguous column references in WHERE clauses
|
|
3249
|
+
const tableAlias = this.targetTable;
|
|
3152
3250
|
for (const [colName, dbColumnName] of columnNameMap) {
|
|
3153
3251
|
Object.defineProperty(mock, colName, {
|
|
3154
3252
|
get() {
|
|
@@ -3157,6 +3255,7 @@ class CollectionQueryBuilder {
|
|
|
3157
3255
|
cached = fieldRefCache[colName] = {
|
|
3158
3256
|
__fieldName: colName,
|
|
3159
3257
|
__dbColumnName: dbColumnName,
|
|
3258
|
+
__tableAlias: tableAlias, // Include table alias for unambiguous references
|
|
3160
3259
|
};
|
|
3161
3260
|
}
|
|
3162
3261
|
return cached;
|
|
@@ -3170,30 +3269,35 @@ class CollectionQueryBuilder {
|
|
|
3170
3269
|
for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
|
|
3171
3270
|
if (relConfig.type === 'many') {
|
|
3172
3271
|
// Collection navigation
|
|
3272
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
3173
3273
|
Object.defineProperty(mock, relName, {
|
|
3174
3274
|
get: () => {
|
|
3175
3275
|
// Don't call build() - it returns schema without relations
|
|
3176
3276
|
const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
|
|
3177
3277
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, undefined, // Don't pass schema, force registry lookup
|
|
3178
3278
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
3279
|
+
// No navigation path needed here - direct collection access from parent
|
|
3179
3280
|
);
|
|
3180
3281
|
},
|
|
3181
|
-
enumerable:
|
|
3282
|
+
enumerable: false,
|
|
3182
3283
|
configurable: true,
|
|
3183
3284
|
});
|
|
3184
3285
|
}
|
|
3185
3286
|
else {
|
|
3186
3287
|
// Reference navigation
|
|
3288
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
3187
3289
|
Object.defineProperty(mock, relName, {
|
|
3188
3290
|
get: () => {
|
|
3189
3291
|
// Don't call build() - it returns schema without relations
|
|
3190
3292
|
// Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
|
|
3191
3293
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
|
|
3192
|
-
this.schemaRegistry // Pass schema registry for nested resolution
|
|
3294
|
+
this.schemaRegistry, // Pass schema registry for nested resolution
|
|
3295
|
+
[], // Empty navigation path - this is the first reference in the chain
|
|
3296
|
+
this.targetTable // Source alias is this collection's target table
|
|
3193
3297
|
);
|
|
3194
3298
|
return refBuilder.createMockTargetRow();
|
|
3195
3299
|
},
|
|
3196
|
-
enumerable:
|
|
3300
|
+
enumerable: false,
|
|
3197
3301
|
configurable: true,
|
|
3198
3302
|
});
|
|
3199
3303
|
}
|
|
@@ -3328,6 +3432,18 @@ class CollectionQueryBuilder {
|
|
|
3328
3432
|
getTargetTableSchema() {
|
|
3329
3433
|
return this.targetTableSchema;
|
|
3330
3434
|
}
|
|
3435
|
+
/**
|
|
3436
|
+
* Get selected field configs (for mapper lookup during transformation)
|
|
3437
|
+
*/
|
|
3438
|
+
getSelectedFieldConfigs() {
|
|
3439
|
+
return this._selectedFieldConfigs;
|
|
3440
|
+
}
|
|
3441
|
+
/**
|
|
3442
|
+
* Get schema registry (for mapper lookup during transformation of navigation fields)
|
|
3443
|
+
*/
|
|
3444
|
+
getSchemaRegistry() {
|
|
3445
|
+
return this.schemaRegistry;
|
|
3446
|
+
}
|
|
3331
3447
|
/**
|
|
3332
3448
|
* Check if this collection uses array aggregation (for flattened results)
|
|
3333
3449
|
*/
|
|
@@ -3613,7 +3729,9 @@ class CollectionQueryBuilder {
|
|
|
3613
3729
|
cteCounter: context.cteCounter,
|
|
3614
3730
|
};
|
|
3615
3731
|
const nestedResult = field.buildCTE(nestedCtx, client);
|
|
3732
|
+
// Sync both counters back - cteCounter for CTE naming, paramCounter for parameter numbering
|
|
3616
3733
|
context.cteCounter = nestedCtx.cteCounter;
|
|
3734
|
+
context.paramCounter = nestedCtx.paramCounter;
|
|
3617
3735
|
// For CTE/LATERAL strategy, we need to track the nested join
|
|
3618
3736
|
// The nested aggregation needs to be joined in the outer collection's subquery
|
|
3619
3737
|
if (nestedResult.tableName) {
|
|
@@ -3647,11 +3765,13 @@ class CollectionQueryBuilder {
|
|
|
3647
3765
|
// FieldRef object - use database column name with optional table alias
|
|
3648
3766
|
const dbColumnName = field.__dbColumnName;
|
|
3649
3767
|
const tableAlias = field.__tableAlias;
|
|
3768
|
+
const fieldName = field.__fieldName; // Property name for mapper lookup
|
|
3769
|
+
const sourceTable = field.__sourceTable; // Actual table name for schema lookup
|
|
3650
3770
|
// If tableAlias differs from the target table, it's a navigation property reference
|
|
3651
3771
|
if (tableAlias && tableAlias !== this.targetTable) {
|
|
3652
|
-
return { alias, expression: `"${tableAlias}"."${dbColumnName}"
|
|
3772
|
+
return { alias, expression: `"${tableAlias}"."${dbColumnName}"`, propertyName: fieldName, sourceTable };
|
|
3653
3773
|
}
|
|
3654
|
-
return { alias, expression: `"${dbColumnName}"
|
|
3774
|
+
return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName };
|
|
3655
3775
|
}
|
|
3656
3776
|
else if (typeof field === 'string') {
|
|
3657
3777
|
// Simple string reference (for backward compatibility)
|
|
@@ -3682,9 +3802,11 @@ class CollectionQueryBuilder {
|
|
|
3682
3802
|
// Single field selection - use the field name as both alias and expression
|
|
3683
3803
|
const field = selectedFields;
|
|
3684
3804
|
const dbColumnName = field.__dbColumnName;
|
|
3805
|
+
const fieldName = field.__fieldName; // Property name for mapper lookup
|
|
3685
3806
|
selectedFieldConfigs.push({
|
|
3686
3807
|
alias: dbColumnName,
|
|
3687
3808
|
expression: `"${dbColumnName}"`,
|
|
3809
|
+
propertyName: fieldName,
|
|
3688
3810
|
});
|
|
3689
3811
|
}
|
|
3690
3812
|
else {
|
|
@@ -3703,6 +3825,7 @@ class CollectionQueryBuilder {
|
|
|
3703
3825
|
selectedFieldConfigs.push({
|
|
3704
3826
|
alias: colName,
|
|
3705
3827
|
expression: `"${dbColumnName}"`,
|
|
3828
|
+
propertyName: colName, // Same as alias when selecting all fields
|
|
3706
3829
|
});
|
|
3707
3830
|
}
|
|
3708
3831
|
}
|
|
@@ -3714,6 +3837,8 @@ class CollectionQueryBuilder {
|
|
|
3714
3837
|
});
|
|
3715
3838
|
}
|
|
3716
3839
|
}
|
|
3840
|
+
// Cache selected field configs for mapper lookup during transformation
|
|
3841
|
+
this._selectedFieldConfigs = selectedFieldConfigs;
|
|
3717
3842
|
// Step 2: Build WHERE clause SQL (without WHERE keyword)
|
|
3718
3843
|
let whereClause;
|
|
3719
3844
|
let whereParams;
|
|
@@ -3780,6 +3905,12 @@ class CollectionQueryBuilder {
|
|
|
3780
3905
|
const selectedFields = this.selector(mockItem);
|
|
3781
3906
|
this.detectNavigationJoins(selectedFields, navigationJoins, this.targetTable, this.targetTableSchema);
|
|
3782
3907
|
}
|
|
3908
|
+
// Step 5b: Merge navigation path joins (for intermediate tables in navigation chains)
|
|
3909
|
+
// These joins are needed when accessing a collection through a chain like:
|
|
3910
|
+
// oi.productPrice.product.resort.productIntegrationDefinitions
|
|
3911
|
+
// The navigation path contains joins for productPrice, product, resort
|
|
3912
|
+
// which must be included in the lateral subquery for correlation
|
|
3913
|
+
const allNavigationJoins = [...this.navigationPath, ...navigationJoins];
|
|
3783
3914
|
// Step 6: Build CollectionAggregationConfig object
|
|
3784
3915
|
const config = {
|
|
3785
3916
|
relationName: this.relationName,
|
|
@@ -3799,7 +3930,7 @@ class CollectionQueryBuilder {
|
|
|
3799
3930
|
arrayField,
|
|
3800
3931
|
defaultValue,
|
|
3801
3932
|
counter: context.cteCounter++,
|
|
3802
|
-
navigationJoins:
|
|
3933
|
+
navigationJoins: allNavigationJoins.length > 0 ? allNavigationJoins : undefined,
|
|
3803
3934
|
};
|
|
3804
3935
|
// Step 6: Call the strategy
|
|
3805
3936
|
const result = strategy.buildAggregation(config, context, client);
|