bigal 15.4.0 → 15.6.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [15.6.0](https://github.com/bigalorm/bigal/compare/v15.5.0...v15.6.0) (2026-01-18)
2
+
3
+ ### Features
4
+
5
+ - Add support for joining to subqueries with GROUP BY and aggregates ([#275](https://github.com/bigalorm/bigal/issues/275)) ([6108663](https://github.com/bigalorm/bigal/commit/6108663f29f22bc4abb87028f9537305c69055b7))
6
+
7
+ # [15.5.0](https://github.com/bigalorm/bigal/compare/v15.4.0...v15.5.0) (2026-01-13)
8
+
9
+ ### Features
10
+
11
+ - Add toJSON() method for returning plain objects instead of class instances ([#271](https://github.com/bigalorm/bigal/issues/271)) ([3e2ee5d](https://github.com/bigalorm/bigal/commit/3e2ee5d803d705bc54bc8b08044f5848088563bb))
12
+
1
13
  # [15.4.0](https://github.com/bigalorm/bigal/compare/v15.3.0...v15.4.0) (2026-01-08)
2
14
 
3
15
  ### Features
package/README.md CHANGED
@@ -183,8 +183,8 @@ export function startup({
183
183
  });
184
184
 
185
185
  let categoryRepository: Repository<Category>;
186
- let productRepository: Repository<Category>;
187
- let storeRepository: Repository<Category>;
186
+ let productRepository: Repository<Product>;
187
+ let storeRepository: Repository<Store>;
188
188
  for (const [modelName, repository] = Object.entries(repositoriesByName)) {
189
189
  switch (modelName) {
190
190
  case 'Category':
@@ -803,6 +803,185 @@ const orders = await OrderRepository.find().where({ store: { in: activeStoreIds
803
803
 
804
804
  ---
805
805
 
806
+ #### Joining to Subqueries
807
+
808
+ You can join to subqueries (derived tables) using `join()` or `leftJoin()`. This is useful for aggregating data and joining it back to the main query.
809
+
810
+ ##### Basic subquery join with COUNT aggregate
811
+
812
+ ```ts
813
+ import { subquery } from 'bigal';
814
+
815
+ // Count products per store
816
+ const productCounts = subquery(ProductRepository)
817
+ .select(['store', (s) => s.count().as('productCount')])
818
+ .groupBy(['store']);
819
+
820
+ // Join to get stores with their product counts
821
+ const stores = await StoreRepository.find().join(productCounts, 'productStats', { on: { id: 'store' } });
822
+ ```
823
+
824
+ Equivalent SQL:
825
+
826
+ ```sql
827
+ SELECT * FROM stores
828
+ INNER JOIN (
829
+ SELECT store_id AS store, COUNT(*) AS "productCount"
830
+ FROM products
831
+ GROUP BY store_id
832
+ ) AS "productStats" ON stores.id = "productStats".store
833
+ ```
834
+
835
+ ##### LEFT JOIN to subquery
836
+
837
+ Use `leftJoin()` to include rows even when there's no matching subquery row:
838
+
839
+ ```ts
840
+ const productCounts = subquery(ProductRepository)
841
+ .select(['store', (s) => s.count().as('productCount')])
842
+ .groupBy(['store']);
843
+
844
+ // LEFT JOIN returns all stores, even those with no products
845
+ const stores = await StoreRepository.find().leftJoin(productCounts, 'productStats', { on: { id: 'store' } });
846
+ ```
847
+
848
+ ##### Sorting by subquery columns
849
+
850
+ Use dot notation to sort by columns from the subquery:
851
+
852
+ ```ts
853
+ const productCounts = subquery(ProductRepository)
854
+ .select(['store', (s) => s.count().as('productCount')])
855
+ .groupBy(['store']);
856
+
857
+ // Sort by product count descending (most products first)
858
+ const stores = await StoreRepository.find()
859
+ .join(productCounts, 'productStats', { on: { id: 'store' } })
860
+ .sort('productStats.productCount desc');
861
+ ```
862
+
863
+ ##### Subquery with WHERE clause and aggregate
864
+
865
+ Filter within the subquery before aggregating:
866
+
867
+ ```ts
868
+ // Count only active products per store
869
+ const activeProductCounts = subquery(ProductRepository)
870
+ .select(['store', (s) => s.count().as('activeCount')])
871
+ .where({ isActive: true })
872
+ .groupBy(['store']);
873
+
874
+ const stores = await StoreRepository.find().join(activeProductCounts, 'activeStats', { on: { id: 'store' } });
875
+ ```
876
+
877
+ ##### COUNT DISTINCT in subquery
878
+
879
+ Use `.distinct()` for counting unique values:
880
+
881
+ ```ts
882
+ // Count unique product names per store
883
+ const uniqueNameCounts = subquery(ProductRepository)
884
+ .select(['store', (s) => s.count('name').distinct().as('uniqueNames')])
885
+ .groupBy(['store']);
886
+
887
+ const stores = await StoreRepository.find().join(uniqueNameCounts, 'stats', { on: { id: 'store' } });
888
+ ```
889
+
890
+ Equivalent SQL:
891
+
892
+ ```sql
893
+ SELECT * FROM stores
894
+ INNER JOIN (
895
+ SELECT store_id AS store, COUNT(DISTINCT name) AS "uniqueNames"
896
+ FROM products
897
+ GROUP BY store_id
898
+ ) AS stats ON stores.id = stats.store
899
+ ```
900
+
901
+ ##### Multiple aggregates in a single subquery
902
+
903
+ Compute multiple aggregates in one subquery:
904
+
905
+ ```ts
906
+ const orderStats = subquery(OrderRepository)
907
+ .select(['store', (s) => s.count().as('orderCount'), (s) => s.sum('total').as('totalRevenue'), (s) => s.avg('total').as('avgOrderValue')])
908
+ .groupBy(['store']);
909
+
910
+ const stores = await StoreRepository.find()
911
+ .join(orderStats, 'stats', { on: { id: 'store' } })
912
+ .sort('stats.totalRevenue desc');
913
+ ```
914
+
915
+ ##### Default aggregate aliases
916
+
917
+ Aggregate functions have default aliases matching their function name:
918
+
919
+ ```ts
920
+ // Without .as() - alias defaults to function name
921
+ const counts = subquery(ProductRepository)
922
+ .select(['store', (s) => s.count()]) // alias: "count"
923
+ .groupBy(['store']);
924
+
925
+ // With .as() - custom alias
926
+ const counts = subquery(ProductRepository)
927
+ .select(['store', (s) => s.count().as('productCount')]) // alias: "productCount"
928
+ .groupBy(['store']);
929
+ ```
930
+
931
+ ##### HAVING clause for filtering aggregated results
932
+
933
+ Use `.having()` to filter groups based on aggregate values:
934
+
935
+ ```ts
936
+ // Only include stores with more than 10 products
937
+ const productCounts = subquery(ProductRepository)
938
+ .select(['store', (s) => s.count().as('productCount')])
939
+ .groupBy(['store'])
940
+ .having({ productCount: { '>': 10 } });
941
+
942
+ const popularStores = await StoreRepository.find().join(productCounts, 'stats', { on: { id: 'store' } });
943
+ ```
944
+
945
+ Supported comparison operators: `>`, `>=`, `<`, `<=`, `!=`, or exact equality (number).
946
+
947
+ ```ts
948
+ // Multiple conditions on different aggregates
949
+ const orderStats = subquery(OrderRepository)
950
+ .select(['store', (s) => s.count().as('orderCount'), (s) => s.avg('total').as('avgOrderValue')])
951
+ .groupBy(['store'])
952
+ .having({ orderCount: { '>=': 5 }, avgOrderValue: { '>': 100 } });
953
+ ```
954
+
955
+ ##### WHERE vs HAVING in subqueries
956
+
957
+ `WHERE` and `HAVING` serve different purposes in aggregated subqueries:
958
+
959
+ - **WHERE** filters individual rows _before_ grouping
960
+ - **HAVING** filters groups _after_ aggregation
961
+
962
+ ```ts
963
+ // WHERE: Only count active products (filters rows before counting)
964
+ const activeProductCounts = subquery(ProductRepository)
965
+ .select(['store', (s) => s.count().as('productCount')])
966
+ .where({ isActive: true }) // Excludes inactive products from the count
967
+ .groupBy(['store']);
968
+
969
+ // HAVING: Only include stores with high product counts (filters groups after counting)
970
+ const highVolumeStores = subquery(ProductRepository)
971
+ .select(['store', (s) => s.count().as('productCount')])
972
+ .groupBy(['store'])
973
+ .having({ productCount: { '>': 100 } }); // Excludes stores with 100 or fewer products
974
+
975
+ // Combined: Count active products, then filter to stores with many active products
976
+ const highVolumeActiveStores = subquery(ProductRepository)
977
+ .select(['store', (s) => s.count().as('activeCount')])
978
+ .where({ isActive: true }) // Step 1: Only consider active products
979
+ .groupBy(['store'])
980
+ .having({ activeCount: { '>': 50 } }); // Step 2: Only keep stores with >50 active products
981
+ ```
982
+
983
+ ---
984
+
806
985
  ### `.count()` - Get the number of records matching the where criteria
807
986
 
808
987
  ```ts