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.
@@ -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
- if (!columnCache || columnCache.size === 0) {
2679
- // Fallback for schemas without cache (shouldn't happen, but be safe)
2680
- return items.map(item => {
2681
- const transformedItem = {};
2682
- for (const [key, value] of Object.entries(item)) {
2683
- const column = targetSchema.columns[key];
2684
- if (column) {
2685
- const config = column.build();
2686
- transformedItem[key] = config.mapper
2687
- ? config.mapper.fromDriver(value)
2688
- : value;
2689
- }
2690
- else {
2691
- transformedItem[key] = value;
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
- return transformedItem;
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
- // Optimized path using cached metadata and while(i--) loop
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 cached = columnCache.get(key);
2706
- if (cached && cached.hasMapper) {
2707
- transformedItem[key] = cached.mapper.fromDriver(value);
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, // Mark which table this belongs to
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.targetTable, nestedTargetSchema, // Pass the target schema directly
3076
- this.schemaRegistry // Pass schema registry for nested resolution
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: navigationJoins.length > 0 ? navigationJoins : undefined,
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);