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 +12 -0
- package/README.md +181 -2
- package/dist/index.cjs +425 -55
- package/dist/index.d.cts +193 -42
- package/dist/index.d.mts +193 -42
- package/dist/index.d.ts +193 -42
- package/dist/index.mjs +424 -56
- package/package.json +4 -4
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<
|
|
187
|
-
let storeRepository: Repository<
|
|
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
|