linkgress-orm 0.4.2 → 0.4.3

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.
@@ -1757,6 +1757,8 @@ class SelectQueryBuilder {
1757
1757
  return "'{}'";
1758
1758
  case 'count':
1759
1759
  return '0';
1760
+ case 'exists':
1761
+ return 'false';
1760
1762
  case 'min':
1761
1763
  case 'max':
1762
1764
  case 'sum':
@@ -1772,7 +1774,11 @@ class SelectQueryBuilder {
1772
1774
  const builderAny = builder;
1773
1775
  if (builderAny.aggregationType) {
1774
1776
  // Scalar aggregation
1775
- return builderAny.aggregationType === 'COUNT' ? 0 : null;
1777
+ if (builderAny.aggregationType === 'COUNT')
1778
+ return 0;
1779
+ if (builderAny.aggregationType === 'EXISTS')
1780
+ return false;
1781
+ return null;
1776
1782
  }
1777
1783
  else if (builderAny.flattenResultType) {
1778
1784
  // Array aggregation
@@ -3450,6 +3456,9 @@ ${joinClauses.join('\n')}`;
3450
3456
  if (aggregationType === 'COUNT') {
3451
3457
  selectParts.push(`COALESCE("${cteName}".data, 0) as "${name}"`);
3452
3458
  }
3459
+ else if (aggregationType === 'EXISTS') {
3460
+ selectParts.push(`COALESCE("${cteName}".data, false) as "${name}"`);
3461
+ }
3453
3462
  else {
3454
3463
  // For MAX/MIN/SUM, keep NULL as-is
3455
3464
  selectParts.push(`"${cteName}".data as "${name}"`);
@@ -4735,6 +4744,8 @@ class CollectionQueryBuilder {
4735
4744
  this.orderByFields = [];
4736
4745
  this.isMarkedAsList = false;
4737
4746
  this.isDistinct = false;
4747
+ // Navigation joins added by selectMany() - included in both CTE and LATERAL navigation joins
4748
+ this.selectManyJoins = [];
4738
4749
  this.relationName = relationName;
4739
4750
  this.targetTable = targetTable;
4740
4751
  this.targetTableSchema = targetTableSchema;
@@ -4768,6 +4779,8 @@ class CollectionQueryBuilder {
4768
4779
  newBuilder.orderByFields = this.orderByFields;
4769
4780
  newBuilder.asName = this.asName;
4770
4781
  newBuilder.isDistinct = this.isDistinct;
4782
+ newBuilder.selectManyJoins = this.selectManyJoins;
4783
+ newBuilder.foreignKeyTableAlias = this.foreignKeyTableAlias;
4771
4784
  return newBuilder;
4772
4785
  }
4773
4786
  /**
@@ -4946,6 +4959,48 @@ class CollectionQueryBuilder {
4946
4959
  this.aggregationType = 'COUNT';
4947
4960
  return this;
4948
4961
  }
4962
+ /**
4963
+ * Check if any items exist in the collection
4964
+ * Returns SqlFragment<boolean> for automatic type resolution in selectors
4965
+ */
4966
+ exists() {
4967
+ this.aggregationType = 'EXISTS';
4968
+ return this;
4969
+ }
4970
+ /**
4971
+ * Flatten a nested collection through this collection.
4972
+ * Similar to LINQ's SelectMany - projects each item to a collection and flattens.
4973
+ *
4974
+ * Example: product.productPrices!.selectMany(pp => pp.capacityGroups!).exists()
4975
+ * SQL: SELECT EXISTS(SELECT 1 FROM capacity_groups JOIN product_prices ON ... WHERE ...)
4976
+ */
4977
+ selectMany(selector) {
4978
+ const mockItem = this.createMockItem();
4979
+ const innerCollection = selector(mockItem);
4980
+ const innerAny = innerCollection;
4981
+ // Build a navigation join from inner target table → this (intermediate) table
4982
+ // e.g., product_price_capacity_groups.product_price_id → product_prices.id
4983
+ const navJoin = {
4984
+ alias: this.targetTable,
4985
+ targetTable: this.targetTable,
4986
+ foreignKeys: [innerAny.foreignKey], // FK on inner table pointing to intermediate
4987
+ matches: ['id'], // PK on intermediate table
4988
+ isMandatory: true, // INNER JOIN for flattening
4989
+ sourceAlias: innerAny.targetTable, // Source is the inner (target) table
4990
+ };
4991
+ // Create new builder targeting the inner table but with outer's FK for parent correlation
4992
+ const newBuilder = new CollectionQueryBuilder(this.relationName, // Keep outer relation name for CTE naming
4993
+ innerAny.targetTable, // Target is the inner collection's table
4994
+ this.foreignKey, // FK is the outer collection's FK (e.g., product_id)
4995
+ this.sourceTable, // Source is the outer collection's source (e.g., products)
4996
+ innerAny.targetTableSchema, this.schemaRegistry, this.navigationPath);
4997
+ // The FK column lives on the intermediate table, not the target table
4998
+ newBuilder.selectManyJoins = [navJoin];
4999
+ newBuilder.foreignKeyTableAlias = this.targetTable;
5000
+ // Carry over where condition from outer collection if any
5001
+ newBuilder.whereCond = this.whereCond;
5002
+ return newBuilder;
5003
+ }
4949
5004
  /**
4950
5005
  * Flatten result to number array (for single-column selections)
4951
5006
  */
@@ -5360,7 +5415,7 @@ class CollectionQueryBuilder {
5360
5415
  // For scalar aggregations (count, min, max, sum), don't include nestedCollectionInfo
5361
5416
  // because the result is a scalar value, not a structured object that needs transformation
5362
5417
  const aggregationType = field.getAggregationType();
5363
- const isScalarAggregation = aggregationType && ['COUNT', 'MIN', 'MAX', 'SUM'].includes(aggregationType);
5418
+ const isScalarAggregation = aggregationType && ['COUNT', 'MIN', 'MAX', 'SUM', 'EXISTS'].includes(aggregationType);
5364
5419
  if (isScalarAggregation) {
5365
5420
  // Scalar aggregation - just return the expression, no nested transformation needed
5366
5421
  return {
@@ -5509,10 +5564,10 @@ class CollectionQueryBuilder {
5509
5564
  let arrayField;
5510
5565
  let defaultValue;
5511
5566
  if (this.aggregationType) {
5512
- // Scalar aggregations: count, min, max, sum
5567
+ // Scalar aggregations: count, min, max, sum, exists
5513
5568
  aggregationType = this.aggregationType.toLowerCase();
5514
- // For aggregations other than COUNT, determine which field to aggregate
5515
- if (this.aggregationType !== 'COUNT' && this.selector) {
5569
+ // For aggregations other than COUNT and EXISTS, determine which field to aggregate
5570
+ if (this.aggregationType !== 'COUNT' && this.aggregationType !== 'EXISTS' && this.selector) {
5516
5571
  const mockItem = this.createMockItem();
5517
5572
  const selectedField = this.selector(mockItem);
5518
5573
  if (typeof selectedField === 'object' && selectedField !== null && '__dbColumnName' in selectedField) {
@@ -5520,7 +5575,15 @@ class CollectionQueryBuilder {
5520
5575
  }
5521
5576
  }
5522
5577
  // Set default value based on aggregation type
5523
- defaultValue = aggregationType === 'count' ? '0' : 'null';
5578
+ if (aggregationType === 'count') {
5579
+ defaultValue = '0';
5580
+ }
5581
+ else if (aggregationType === 'exists') {
5582
+ defaultValue = 'false';
5583
+ }
5584
+ else {
5585
+ defaultValue = 'null';
5586
+ }
5524
5587
  }
5525
5588
  else if (this.flattenResultType) {
5526
5589
  // Array aggregation for toNumberList/toStringList
@@ -5550,12 +5613,16 @@ class CollectionQueryBuilder {
5550
5613
  // oi.productPrice.product.resort.productIntegrationDefinitions
5551
5614
  // The navigation path contains joins for productPrice, product, resort
5552
5615
  // which must be included in the lateral subquery for correlation
5553
- const allNavigationJoins = [...this.navigationPath, ...navigationJoins];
5616
+ // Include selectMany joins in both all and selector navigation joins
5617
+ // selectMany joins are structural (from flattening) and needed by both CTE and LATERAL
5618
+ const allNavigationJoins = [...this.navigationPath, ...this.selectManyJoins, ...navigationJoins];
5619
+ const allSelectorJoins = [...this.selectManyJoins, ...navigationJoins];
5554
5620
  // Step 6: Build CollectionAggregationConfig object
5555
5621
  const config = {
5556
5622
  relationName: this.relationName,
5557
5623
  targetTable: this.targetTable,
5558
5624
  foreignKey: this.foreignKey,
5625
+ foreignKeyTableAlias: this.foreignKeyTableAlias,
5559
5626
  sourceTable: this.sourceTable,
5560
5627
  parentIds, // Pass parent IDs for temp table strategy
5561
5628
  selectedFields: selectedFieldConfigs,
@@ -5575,7 +5642,7 @@ class CollectionQueryBuilder {
5575
5642
  // Use the reserved counter for LATERAL strategy, otherwise increment as before
5576
5643
  counter: reservedCounter !== undefined ? reservedCounter : context.cteCounter++,
5577
5644
  navigationJoins: allNavigationJoins.length > 0 ? allNavigationJoins : undefined,
5578
- selectorNavigationJoins: navigationJoins.length > 0 ? navigationJoins : undefined,
5645
+ selectorNavigationJoins: allSelectorJoins.length > 0 ? allSelectorJoins : undefined,
5579
5646
  };
5580
5647
  // Step 6: Call the strategy
5581
5648
  const result = strategy.buildAggregation(config, context, client);