linkgress-orm 0.1.19 → 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/entity/db-context.d.ts +4 -1
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +47 -24
- package/dist/entity/db-context.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 +123 -32
- package/dist/query/query-builder.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 +1 -1
|
@@ -2675,26 +2675,58 @@ class SelectQueryBuilder {
|
|
|
2675
2675
|
// Use pre-cached column metadata from target schema
|
|
2676
2676
|
// This avoids repeated column.build() calls for each item
|
|
2677
2677
|
const columnCache = targetSchema.columnMetadataCache;
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
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;
|
|
2692
2707
|
}
|
|
2693
2708
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
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
|
+
}
|
|
2696
2728
|
}
|
|
2697
|
-
//
|
|
2729
|
+
// Transform items using pre-built mapper cache
|
|
2698
2730
|
const results = new Array(items.length);
|
|
2699
2731
|
let i = items.length;
|
|
2700
2732
|
while (i--) {
|
|
@@ -2702,9 +2734,9 @@ class SelectQueryBuilder {
|
|
|
2702
2734
|
const transformedItem = {};
|
|
2703
2735
|
for (const key in item) {
|
|
2704
2736
|
const value = item[key];
|
|
2705
|
-
const
|
|
2706
|
-
if (
|
|
2707
|
-
transformedItem[key] =
|
|
2737
|
+
const mapper = mapperCache.get(key);
|
|
2738
|
+
if (mapper) {
|
|
2739
|
+
transformedItem[key] = mapper.fromDriver(value);
|
|
2708
2740
|
}
|
|
2709
2741
|
else {
|
|
2710
2742
|
transformedItem[key] = value;
|
|
@@ -2964,13 +2996,15 @@ exports.SelectQueryBuilder = SelectQueryBuilder;
|
|
|
2964
2996
|
* Reference query builder for single navigation (many-to-one, one-to-one)
|
|
2965
2997
|
*/
|
|
2966
2998
|
class ReferenceQueryBuilder {
|
|
2967
|
-
constructor(relationName, targetTable, foreignKeys, matches, isMandatory, targetTableSchema, schemaRegistry) {
|
|
2999
|
+
constructor(relationName, targetTable, foreignKeys, matches, isMandatory, targetTableSchema, schemaRegistry, navigationPath, sourceAlias) {
|
|
2968
3000
|
this.relationName = relationName;
|
|
2969
3001
|
this.targetTable = targetTable;
|
|
2970
3002
|
this.foreignKeys = foreignKeys;
|
|
2971
3003
|
this.matches = matches;
|
|
2972
3004
|
this.isMandatory = isMandatory;
|
|
2973
3005
|
this.schemaRegistry = schemaRegistry;
|
|
3006
|
+
this.navigationPath = navigationPath || [];
|
|
3007
|
+
this.sourceAlias = sourceAlias || '';
|
|
2974
3008
|
// Prefer registry lookup (has full relations) over passed schema
|
|
2975
3009
|
if (this.schemaRegistry) {
|
|
2976
3010
|
this.targetTableSchema = this.schemaRegistry.get(targetTable);
|
|
@@ -3036,6 +3070,7 @@ class ReferenceQueryBuilder {
|
|
|
3036
3070
|
columnMappers[colName] = config.mapper;
|
|
3037
3071
|
}
|
|
3038
3072
|
}
|
|
3073
|
+
const sourceTable = this.targetTable; // Actual table name for schema lookup
|
|
3039
3074
|
for (const [colName, dbColumnName] of columnNameMap) {
|
|
3040
3075
|
const mapper = columnMappers[colName];
|
|
3041
3076
|
Object.defineProperty(mock, colName, {
|
|
@@ -3045,7 +3080,8 @@ class ReferenceQueryBuilder {
|
|
|
3045
3080
|
cached = fieldRefCache[colName] = {
|
|
3046
3081
|
__fieldName: colName,
|
|
3047
3082
|
__dbColumnName: dbColumnName,
|
|
3048
|
-
__tableAlias: tableAlias, //
|
|
3083
|
+
__tableAlias: tableAlias, // Alias for SQL generation
|
|
3084
|
+
__sourceTable: sourceTable, // Actual table name for mapper lookup
|
|
3049
3085
|
__mapper: mapper, // Include mapper for toDriver transformation in conditions
|
|
3050
3086
|
};
|
|
3051
3087
|
}
|
|
@@ -3055,6 +3091,23 @@ class ReferenceQueryBuilder {
|
|
|
3055
3091
|
configurable: true,
|
|
3056
3092
|
});
|
|
3057
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
|
+
}
|
|
3058
3111
|
// Add navigation properties (both collections and references)
|
|
3059
3112
|
if (this.targetTableSchema.relations) {
|
|
3060
3113
|
for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
|
|
@@ -3072,8 +3125,10 @@ class ReferenceQueryBuilder {
|
|
|
3072
3125
|
Object.defineProperty(mock, relName, {
|
|
3073
3126
|
get: () => {
|
|
3074
3127
|
const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
|
|
3075
|
-
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.
|
|
3076
|
-
|
|
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)
|
|
3077
3132
|
);
|
|
3078
3133
|
},
|
|
3079
3134
|
enumerable: false,
|
|
@@ -3087,7 +3142,9 @@ class ReferenceQueryBuilder {
|
|
|
3087
3142
|
Object.defineProperty(mock, relName, {
|
|
3088
3143
|
get: () => {
|
|
3089
3144
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
|
|
3090
|
-
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
|
|
3091
3148
|
);
|
|
3092
3149
|
return refBuilder.createMockTargetRow();
|
|
3093
3150
|
},
|
|
@@ -3110,7 +3167,7 @@ exports.ReferenceQueryBuilder = ReferenceQueryBuilder;
|
|
|
3110
3167
|
* Collection query builder for nested queries
|
|
3111
3168
|
*/
|
|
3112
3169
|
class CollectionQueryBuilder {
|
|
3113
|
-
constructor(relationName, targetTable, foreignKey, sourceTable, targetTableSchema, schemaRegistry) {
|
|
3170
|
+
constructor(relationName, targetTable, foreignKey, sourceTable, targetTableSchema, schemaRegistry, navigationPath) {
|
|
3114
3171
|
this.orderByFields = [];
|
|
3115
3172
|
this.isMarkedAsList = false;
|
|
3116
3173
|
this.isDistinct = false;
|
|
@@ -3120,6 +3177,7 @@ class CollectionQueryBuilder {
|
|
|
3120
3177
|
this.foreignKey = foreignKey;
|
|
3121
3178
|
this.sourceTable = sourceTable;
|
|
3122
3179
|
this.schemaRegistry = schemaRegistry;
|
|
3180
|
+
this.navigationPath = navigationPath || [];
|
|
3123
3181
|
// Prefer registry lookup (has full relations) over passed schema
|
|
3124
3182
|
if (this.schemaRegistry) {
|
|
3125
3183
|
const registrySchema = this.schemaRegistry.get(targetTable);
|
|
@@ -3136,7 +3194,8 @@ class CollectionQueryBuilder {
|
|
|
3136
3194
|
* Select specific fields from collection items
|
|
3137
3195
|
*/
|
|
3138
3196
|
select(selector) {
|
|
3139
|
-
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
|
|
3140
3199
|
);
|
|
3141
3200
|
newBuilder.selector = selector;
|
|
3142
3201
|
newBuilder.whereCond = this.whereCond;
|
|
@@ -3186,7 +3245,8 @@ class CollectionQueryBuilder {
|
|
|
3186
3245
|
const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
3187
3246
|
// Performance: Lazy-cache FieldRef objects
|
|
3188
3247
|
const fieldRefCache = {};
|
|
3189
|
-
// Add columns
|
|
3248
|
+
// Add columns - include tableAlias for unambiguous column references in WHERE clauses
|
|
3249
|
+
const tableAlias = this.targetTable;
|
|
3190
3250
|
for (const [colName, dbColumnName] of columnNameMap) {
|
|
3191
3251
|
Object.defineProperty(mock, colName, {
|
|
3192
3252
|
get() {
|
|
@@ -3195,6 +3255,7 @@ class CollectionQueryBuilder {
|
|
|
3195
3255
|
cached = fieldRefCache[colName] = {
|
|
3196
3256
|
__fieldName: colName,
|
|
3197
3257
|
__dbColumnName: dbColumnName,
|
|
3258
|
+
__tableAlias: tableAlias, // Include table alias for unambiguous references
|
|
3198
3259
|
};
|
|
3199
3260
|
}
|
|
3200
3261
|
return cached;
|
|
@@ -3215,6 +3276,7 @@ class CollectionQueryBuilder {
|
|
|
3215
3276
|
const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
|
|
3216
3277
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, undefined, // Don't pass schema, force registry lookup
|
|
3217
3278
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
3279
|
+
// No navigation path needed here - direct collection access from parent
|
|
3218
3280
|
);
|
|
3219
3281
|
},
|
|
3220
3282
|
enumerable: false,
|
|
@@ -3229,7 +3291,9 @@ class CollectionQueryBuilder {
|
|
|
3229
3291
|
// Don't call build() - it returns schema without relations
|
|
3230
3292
|
// Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
|
|
3231
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
|
|
3232
|
-
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
|
|
3233
3297
|
);
|
|
3234
3298
|
return refBuilder.createMockTargetRow();
|
|
3235
3299
|
},
|
|
@@ -3368,6 +3432,18 @@ class CollectionQueryBuilder {
|
|
|
3368
3432
|
getTargetTableSchema() {
|
|
3369
3433
|
return this.targetTableSchema;
|
|
3370
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
|
+
}
|
|
3371
3447
|
/**
|
|
3372
3448
|
* Check if this collection uses array aggregation (for flattened results)
|
|
3373
3449
|
*/
|
|
@@ -3653,7 +3729,9 @@ class CollectionQueryBuilder {
|
|
|
3653
3729
|
cteCounter: context.cteCounter,
|
|
3654
3730
|
};
|
|
3655
3731
|
const nestedResult = field.buildCTE(nestedCtx, client);
|
|
3732
|
+
// Sync both counters back - cteCounter for CTE naming, paramCounter for parameter numbering
|
|
3656
3733
|
context.cteCounter = nestedCtx.cteCounter;
|
|
3734
|
+
context.paramCounter = nestedCtx.paramCounter;
|
|
3657
3735
|
// For CTE/LATERAL strategy, we need to track the nested join
|
|
3658
3736
|
// The nested aggregation needs to be joined in the outer collection's subquery
|
|
3659
3737
|
if (nestedResult.tableName) {
|
|
@@ -3687,11 +3765,13 @@ class CollectionQueryBuilder {
|
|
|
3687
3765
|
// FieldRef object - use database column name with optional table alias
|
|
3688
3766
|
const dbColumnName = field.__dbColumnName;
|
|
3689
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
|
|
3690
3770
|
// If tableAlias differs from the target table, it's a navigation property reference
|
|
3691
3771
|
if (tableAlias && tableAlias !== this.targetTable) {
|
|
3692
|
-
return { alias, expression: `"${tableAlias}"."${dbColumnName}"
|
|
3772
|
+
return { alias, expression: `"${tableAlias}"."${dbColumnName}"`, propertyName: fieldName, sourceTable };
|
|
3693
3773
|
}
|
|
3694
|
-
return { alias, expression: `"${dbColumnName}"
|
|
3774
|
+
return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName };
|
|
3695
3775
|
}
|
|
3696
3776
|
else if (typeof field === 'string') {
|
|
3697
3777
|
// Simple string reference (for backward compatibility)
|
|
@@ -3722,9 +3802,11 @@ class CollectionQueryBuilder {
|
|
|
3722
3802
|
// Single field selection - use the field name as both alias and expression
|
|
3723
3803
|
const field = selectedFields;
|
|
3724
3804
|
const dbColumnName = field.__dbColumnName;
|
|
3805
|
+
const fieldName = field.__fieldName; // Property name for mapper lookup
|
|
3725
3806
|
selectedFieldConfigs.push({
|
|
3726
3807
|
alias: dbColumnName,
|
|
3727
3808
|
expression: `"${dbColumnName}"`,
|
|
3809
|
+
propertyName: fieldName,
|
|
3728
3810
|
});
|
|
3729
3811
|
}
|
|
3730
3812
|
else {
|
|
@@ -3743,6 +3825,7 @@ class CollectionQueryBuilder {
|
|
|
3743
3825
|
selectedFieldConfigs.push({
|
|
3744
3826
|
alias: colName,
|
|
3745
3827
|
expression: `"${dbColumnName}"`,
|
|
3828
|
+
propertyName: colName, // Same as alias when selecting all fields
|
|
3746
3829
|
});
|
|
3747
3830
|
}
|
|
3748
3831
|
}
|
|
@@ -3754,6 +3837,8 @@ class CollectionQueryBuilder {
|
|
|
3754
3837
|
});
|
|
3755
3838
|
}
|
|
3756
3839
|
}
|
|
3840
|
+
// Cache selected field configs for mapper lookup during transformation
|
|
3841
|
+
this._selectedFieldConfigs = selectedFieldConfigs;
|
|
3757
3842
|
// Step 2: Build WHERE clause SQL (without WHERE keyword)
|
|
3758
3843
|
let whereClause;
|
|
3759
3844
|
let whereParams;
|
|
@@ -3820,6 +3905,12 @@ class CollectionQueryBuilder {
|
|
|
3820
3905
|
const selectedFields = this.selector(mockItem);
|
|
3821
3906
|
this.detectNavigationJoins(selectedFields, navigationJoins, this.targetTable, this.targetTableSchema);
|
|
3822
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];
|
|
3823
3914
|
// Step 6: Build CollectionAggregationConfig object
|
|
3824
3915
|
const config = {
|
|
3825
3916
|
relationName: this.relationName,
|
|
@@ -3839,7 +3930,7 @@ class CollectionQueryBuilder {
|
|
|
3839
3930
|
arrayField,
|
|
3840
3931
|
defaultValue,
|
|
3841
3932
|
counter: context.cteCounter++,
|
|
3842
|
-
navigationJoins:
|
|
3933
|
+
navigationJoins: allNavigationJoins.length > 0 ? allNavigationJoins : undefined,
|
|
3843
3934
|
};
|
|
3844
3935
|
// Step 6: Call the strategy
|
|
3845
3936
|
const result = strategy.buildAggregation(config, context, client);
|