bigal 15.1.0 → 15.3.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/CLAUDE.md +81 -0
- package/README.md +212 -1
- package/dist/index.cjs +636 -76
- package/dist/index.d.cts +212 -110
- package/dist/index.d.mts +212 -110
- package/dist/index.d.ts +212 -110
- package/dist/index.mjs +634 -77
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# [15.3.0](https://github.com/bigalorm/bigal/compare/v15.2.0...v15.3.0) (2026-01-08)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- Add SQL subquery support for WHERE clauses ([#265](https://github.com/bigalorm/bigal/issues/265)) ([2af8d7f](https://github.com/bigalorm/bigal/commit/2af8d7f4ad53642907f3e8d1539bd6f07e1ccecc))
|
|
6
|
+
|
|
7
|
+
# [15.2.0](https://github.com/bigalorm/bigal/compare/v15.1.0...v15.2.0) (2026-01-08)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Add SQL JOIN support for filtering and sorting by related tables ([#263](https://github.com/bigalorm/bigal/issues/263)) ([e5f1a97](https://github.com/bigalorm/bigal/commit/e5f1a97f524d8e784943d179347dc364a62959e1))
|
|
12
|
+
|
|
1
13
|
# [15.1.0](https://github.com/bigalorm/bigal/compare/v15.0.1...v15.1.0) (2026-01-07)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance for Claude Code when working on the BigAl project.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
BigAl is a type-safe PostgreSQL ORM for Node.js/TypeScript. It uses a fluent builder pattern for queries and provides strongly-typed results.
|
|
8
|
+
|
|
9
|
+
## Build and Test Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm run build # Build the project using unbuild
|
|
13
|
+
npm test # Run all tests with Mocha
|
|
14
|
+
npm run lint # Run ESLint and markdownlint
|
|
15
|
+
npx tsc --noEmit # Type-check without emitting files
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Code Style Guidelines
|
|
19
|
+
|
|
20
|
+
### Naming Conventions
|
|
21
|
+
|
|
22
|
+
- Use self-descriptive names for variables, functions, parameters, and types
|
|
23
|
+
- Avoid single-letter variables or abbreviations (use `maxRows` instead of `n`, `parentSubquery` instead of `subquery` when shadowing)
|
|
24
|
+
- Class names should be PascalCase
|
|
25
|
+
- Functions, methods, and variables should be camelCase
|
|
26
|
+
- Constants should be SCREAMING_SNAKE_CASE
|
|
27
|
+
|
|
28
|
+
### Comments
|
|
29
|
+
|
|
30
|
+
- Only add comments when they provide meaningful insight that cannot be inferred from self-describing code
|
|
31
|
+
- Remove or refine unnecessary comments
|
|
32
|
+
- JSDoc is acceptable for public API documentation
|
|
33
|
+
|
|
34
|
+
### TypeScript Practices
|
|
35
|
+
|
|
36
|
+
- Avoid using the `as` keyword for type assertions unless absolutely necessary
|
|
37
|
+
- Prefer type inference where possible
|
|
38
|
+
- Use generic type parameters to narrow types rather than casting (e.g., `max<K extends keyof T>(column: K): T[K]`)
|
|
39
|
+
- Never disable linter rules unless there is no other option - it should be a last resort
|
|
40
|
+
|
|
41
|
+
### Linting and JSDoc
|
|
42
|
+
|
|
43
|
+
- All linting errors and warnings must be resolved, including JSDoc issues
|
|
44
|
+
- Use restraint with examples in JSDoc comments. Only provide an example if usage is unclear.
|
|
45
|
+
- Ensure JSDoc tags are properly escaped if referencing decorators (use backticks or escape with backslash)
|
|
46
|
+
|
|
47
|
+
### Code Organization
|
|
48
|
+
|
|
49
|
+
- Don't create interfaces unnecessarily - if a class can serve as both implementation and type, the interface is redundant
|
|
50
|
+
- Split classes into separate files only when needed to comply with linter rules
|
|
51
|
+
|
|
52
|
+
## Architecture Patterns
|
|
53
|
+
|
|
54
|
+
### Repository Pattern
|
|
55
|
+
|
|
56
|
+
Repositories provide CRUD operations with type-safe query building:
|
|
57
|
+
|
|
58
|
+
- `IReadonlyRepository<T>` - read operations (find, findOne, count)
|
|
59
|
+
- `IRepository<T>` - full CRUD operations
|
|
60
|
+
|
|
61
|
+
### Query Building
|
|
62
|
+
|
|
63
|
+
Queries use a fluent builder pattern with immutable state:
|
|
64
|
+
|
|
65
|
+
- Each method returns a new instance (clone pattern)
|
|
66
|
+
- Methods chain naturally: `.where({...}).sort('name').limit(10)`
|
|
67
|
+
- Queries are `PromiseLike` for automatic execution
|
|
68
|
+
|
|
69
|
+
### SQL Generation
|
|
70
|
+
|
|
71
|
+
- `SqlHelper.ts` handles all SQL string generation
|
|
72
|
+
- Use parameterized queries (`$1`, `$2`, etc.) to prevent SQL injection
|
|
73
|
+
- The `buildWhere()` function recursively processes WHERE clause objects
|
|
74
|
+
|
|
75
|
+
## Testing Conventions
|
|
76
|
+
|
|
77
|
+
- Tests are in `tests/` directory with `.tests.ts` suffix
|
|
78
|
+
- Use Mocha with Chai assertions
|
|
79
|
+
- Test file structure mirrors source structure
|
|
80
|
+
- Repository tests use a shared setup with `repositoriesByModelNameLowered`
|
|
81
|
+
- When test infrastructure types are too generic, type assertions are acceptable
|
package/README.md
CHANGED
|
@@ -9,7 +9,6 @@ A fast, lightweight ORM for PostgreSQL and Node.js, written in TypeScript.
|
|
|
9
9
|
This ORM does not:
|
|
10
10
|
|
|
11
11
|
- Create or update db schemas for you
|
|
12
|
-
- Handle associations/joins
|
|
13
12
|
- Do much else than basic queries, inserts, updates, and deletes
|
|
14
13
|
|
|
15
14
|
## Compatibility
|
|
@@ -461,6 +460,218 @@ const items = await FooRepository.find()
|
|
|
461
460
|
.paginate(page, pageSize);
|
|
462
461
|
```
|
|
463
462
|
|
|
463
|
+
#### Join related tables
|
|
464
|
+
|
|
465
|
+
Use `join()` for INNER JOIN or `leftJoin()` for LEFT JOIN to filter or sort by related table columns in a single query.
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
// INNER JOIN - only returns products that have a store
|
|
469
|
+
const items = await ProductRepository.find()
|
|
470
|
+
.join('store')
|
|
471
|
+
.where({
|
|
472
|
+
store: {
|
|
473
|
+
name: 'Acme',
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
// LEFT JOIN - returns all products, even those without a store
|
|
480
|
+
const items = await ProductRepository.find()
|
|
481
|
+
.leftJoin('store')
|
|
482
|
+
.where({
|
|
483
|
+
store: {
|
|
484
|
+
name: 'Acme',
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
#### Join with alias
|
|
490
|
+
|
|
491
|
+
Use an alias when you need to join the same table multiple times or for clarity.
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
const items = await ProductRepository.find()
|
|
495
|
+
.join('store', 'primaryStore')
|
|
496
|
+
.where({
|
|
497
|
+
primaryStore: {
|
|
498
|
+
name: 'Acme',
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
#### Join with additional ON constraints
|
|
504
|
+
|
|
505
|
+
Add extra conditions to the JOIN's ON clause using `leftJoin()`.
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
const items = await ProductRepository.find()
|
|
509
|
+
.leftJoin('store', 'store', {
|
|
510
|
+
isDeleted: false,
|
|
511
|
+
})
|
|
512
|
+
.where({
|
|
513
|
+
name: {
|
|
514
|
+
like: 'Widget%',
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
#### Sort by joined table columns
|
|
520
|
+
|
|
521
|
+
Use dot notation to sort by columns on joined tables.
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
const items = await ProductRepository.find().join('store').sort('store.name asc');
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
#### Combine multiple where conditions
|
|
528
|
+
|
|
529
|
+
Mix regular where conditions with joined table conditions.
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
const items = await ProductRepository.find()
|
|
533
|
+
.join('store')
|
|
534
|
+
.where({
|
|
535
|
+
name: {
|
|
536
|
+
like: 'Widget%',
|
|
537
|
+
},
|
|
538
|
+
store: {
|
|
539
|
+
name: {
|
|
540
|
+
like: ['Acme', 'foo'],
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
> Note: `join()` and `populate()` serve different purposes. Use `join()` when you need to filter or sort by related
|
|
547
|
+
> table columns in SQL. Use `populate()` when you want to fetch the full related object(s) as nested data in results.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
#### Subqueries
|
|
552
|
+
|
|
553
|
+
Use the `subquery()` function to create subqueries for use in WHERE clauses.
|
|
554
|
+
|
|
555
|
+
##### WHERE IN with subquery
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
import { subquery } from 'bigal';
|
|
559
|
+
|
|
560
|
+
// Find products from active stores
|
|
561
|
+
const activeStoreIds = subquery(StoreRepository).select(['id']).where({ isActive: true });
|
|
562
|
+
|
|
563
|
+
const items = await ProductRepository.find().where({
|
|
564
|
+
store: { in: activeStoreIds },
|
|
565
|
+
});
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
Equivalent SQL:
|
|
569
|
+
|
|
570
|
+
```sql
|
|
571
|
+
SELECT * FROM products
|
|
572
|
+
WHERE store_id IN (SELECT id FROM stores WHERE is_active = $1)
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
##### WHERE NOT IN with subquery
|
|
576
|
+
|
|
577
|
+
Use the existing `!` negation operator:
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
const discontinuedProductIds = subquery(DiscontinuedProductRepository).select(['productId']);
|
|
581
|
+
|
|
582
|
+
const items = await ProductRepository.find().where({
|
|
583
|
+
id: { '!': { in: discontinuedProductIds } },
|
|
584
|
+
});
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Equivalent SQL:
|
|
588
|
+
|
|
589
|
+
```sql
|
|
590
|
+
SELECT * FROM products
|
|
591
|
+
WHERE id NOT IN (SELECT product_id FROM discontinued_products)
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
##### WHERE EXISTS
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
// Find stores that have at least one product
|
|
598
|
+
const items = await StoreRepository.find().where({
|
|
599
|
+
exists: subquery(ProductRepository).where({ storeId: 42 }),
|
|
600
|
+
});
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Equivalent SQL:
|
|
604
|
+
|
|
605
|
+
```sql
|
|
606
|
+
SELECT * FROM stores
|
|
607
|
+
WHERE EXISTS (SELECT 1 FROM products WHERE store_id = $1)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
##### WHERE NOT EXISTS
|
|
611
|
+
|
|
612
|
+
```ts
|
|
613
|
+
// Find stores with no products
|
|
614
|
+
const items = await StoreRepository.find().where({
|
|
615
|
+
'!': {
|
|
616
|
+
exists: subquery(ProductRepository).where({ storeId: 42 }),
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Equivalent SQL:
|
|
622
|
+
|
|
623
|
+
```sql
|
|
624
|
+
SELECT * FROM stores
|
|
625
|
+
WHERE NOT EXISTS (SELECT 1 FROM products WHERE store_id = $1)
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
##### Scalar subquery comparisons
|
|
629
|
+
|
|
630
|
+
Use aggregate methods (`count()`, `avg()`, `sum()`, `max()`, `min()`) to create scalar subqueries for comparisons:
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
// Find products priced above the average
|
|
634
|
+
const avgPrice = subquery(ProductRepository).where({ category: 'electronics' }).avg('price');
|
|
635
|
+
|
|
636
|
+
const items = await ProductRepository.find().where({
|
|
637
|
+
price: { '>': avgPrice },
|
|
638
|
+
});
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Equivalent SQL:
|
|
642
|
+
|
|
643
|
+
```sql
|
|
644
|
+
SELECT * FROM products
|
|
645
|
+
WHERE price > (SELECT AVG(price) FROM products WHERE category = $1)
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
##### Combining subqueries with other conditions
|
|
649
|
+
|
|
650
|
+
Subqueries can be combined with regular where conditions and other operators:
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
const premiumStoreIds = subquery(StoreRepository).select(['id']).where({ tier: 'premium' });
|
|
654
|
+
|
|
655
|
+
const items = await ProductRepository.find().where({
|
|
656
|
+
store: { in: premiumStoreIds },
|
|
657
|
+
price: { '>=': 100 },
|
|
658
|
+
isActive: true,
|
|
659
|
+
});
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
##### Reusable subqueries
|
|
663
|
+
|
|
664
|
+
Subqueries are standalone objects that can be reused across multiple queries:
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
const activeStoreIds = subquery(StoreRepository).select(['id']).where({ isActive: true });
|
|
668
|
+
|
|
669
|
+
// Use in multiple queries
|
|
670
|
+
const products = await ProductRepository.find().where({ store: { in: activeStoreIds } });
|
|
671
|
+
|
|
672
|
+
const orders = await OrderRepository.find().where({ store: { in: activeStoreIds } });
|
|
673
|
+
```
|
|
674
|
+
|
|
464
675
|
---
|
|
465
676
|
|
|
466
677
|
### `.count()` - Get the number of records matching the where criteria
|