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 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