bigal 16.0.0-beta.2 → 16.0.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.
@@ -13,7 +13,7 @@ Use the `subquery()` function:
13
13
  ```ts
14
14
  import { subquery } from 'bigal';
15
15
 
16
- const activeStores = subquery(Store).select(['id']).where({ isActive: true });
16
+ const activeStores = subquery(storeRepository).select(['id']).where({ isActive: true });
17
17
  ```
18
18
 
19
19
  `SubqueryBuilder` methods:
@@ -31,16 +31,16 @@ const activeStores = subquery(Store).select(['id']).where({ isActive: true });
31
31
  ## WHERE IN / NOT IN
32
32
 
33
33
  ```ts
34
- const activeStores = subquery(Store).select(['id']).where({ isActive: true });
34
+ const activeStores = subquery(storeRepository).select(['id']).where({ isActive: true });
35
35
 
36
36
  // WHERE IN
37
- const products = await Product.find().where({
37
+ const products = await productRepository.find().where({
38
38
  store: { in: activeStores },
39
39
  });
40
40
  // SQL: WHERE "store_id" IN (SELECT "id" FROM "stores" WHERE "is_active"=$1)
41
41
 
42
42
  // WHERE NOT IN
43
- const products = await Product.find().where({
43
+ const products = await productRepository.find().where({
44
44
  store: { '!': { in: activeStores } },
45
45
  });
46
46
  ```
@@ -48,15 +48,15 @@ const products = await Product.find().where({
48
48
  ## WHERE EXISTS / NOT EXISTS
49
49
 
50
50
  ```ts
51
- const hasProducts = subquery(Product).where({ name: { like: 'Widget%' } });
51
+ const hasProducts = subquery(productRepository).where({ name: { like: 'Widget%' } });
52
52
 
53
53
  // EXISTS
54
- const stores = await Store.find().where({
54
+ const stores = await storeRepository.find().where({
55
55
  exists: hasProducts,
56
56
  });
57
57
 
58
58
  // NOT EXISTS
59
- const stores = await Store.find().where({
59
+ const stores = await storeRepository.find().where({
60
60
  '!': { exists: hasProducts },
61
61
  });
62
62
  ```
@@ -68,9 +68,9 @@ If no columns are selected in the subquery, it defaults to `SELECT 1`.
68
68
  Compare column values against single-value subquery results:
69
69
 
70
70
  ```ts
71
- const avgPrice = subquery(Product).avg('price');
71
+ const avgPrice = subquery(productRepository).avg('price');
72
72
 
73
- const expensiveProducts = await Product.find().where({
73
+ const expensiveProducts = await productRepository.find().where({
74
74
  price: { '>': avgPrice },
75
75
  });
76
76
  // SQL: WHERE "price">(SELECT AVG("price") FROM "products")
@@ -80,10 +80,10 @@ Supported operators: `>`, `>=`, `<`, `<=`, `'!'` (not equal), or direct equality
80
80
 
81
81
  ```ts
82
82
  // Equal to max price
83
- .where({ price: subquery(Product).max('price') })
83
+ .where({ price: subquery(productRepository).max('price') })
84
84
 
85
85
  // Not equal to min price
86
- .where({ price: { '!': subquery(Product).min('price') } })
86
+ .where({ price: { '!': subquery(productRepository).min('price') } })
87
87
  ```
88
88
 
89
89
  ## Model joins
@@ -92,56 +92,62 @@ Join to related entities defined in your model:
92
92
 
93
93
  ```ts
94
94
  // Inner join
95
- const products = await Product.find()
95
+ const products = await productRepository
96
+ .find()
96
97
  .join('store')
97
98
  .where({ store: { name: 'Acme' } });
98
- // SQL: SELECT "products".* FROM "products"
99
- // INNER JOIN "stores" ON "products"."store_id"="stores"."id"
100
- // WHERE "stores"."name"=$1
99
+ // SQL: SELECT "products"."id","products"."name",… FROM "products"
100
+ // INNER JOIN "stores" AS "store" ON "products"."store_id"="store"."id"
101
+ // WHERE "store"."name"=$1
101
102
 
102
103
  // Left join
103
- const products = await Product.find()
104
+ const products = await productRepository
105
+ .find()
104
106
  .leftJoin('store')
105
107
  .where({ store: { name: 'Acme' } });
106
108
 
107
109
  // Custom alias
108
- const products = await Product.find()
110
+ const products = await productRepository
111
+ .find()
109
112
  .join('store', 'primaryStore')
110
113
  .where({ primaryStore: { name: 'Acme' } });
111
114
 
112
115
  // Additional ON conditions (left join only)
113
- const products = await Product.find().leftJoin('store', 'activeStore', { isActive: true });
116
+ const products = await productRepository.find().leftJoin('store', 'activeStore', { isActive: true });
114
117
  ```
115
118
 
119
+ When a query includes any join, BigAl automatically qualifies the base table's own columns (in `SELECT`, `WHERE`, `ORDER BY`, and `DISTINCT ON`) with the base table name.
120
+ This keeps columns that share a name across the base and joined tables (most commonly `id`) from being ambiguous, so `.join()` is safe to use without falling back to raw SQL.
121
+
116
122
  ## Subquery joins
117
123
 
118
124
  Join to subquery results:
119
125
 
120
126
  ```ts
121
- const productCounts = subquery(Product)
127
+ const productCounts = subquery(productRepository)
122
128
  .select(['store', (sb) => sb.count().as('productCount')])
123
129
  .groupBy(['store']);
124
130
 
125
131
  // Inner join
126
- const stores = await Store.find().join(productCounts, 'stats', { on: { id: 'store' } });
127
- // SQL: SELECT "stores".* FROM "stores"
132
+ const stores = await storeRepository.find().join(productCounts, 'stats', { on: { id: 'store' } });
133
+ // SQL: SELECT "stores"."id","stores"."name" FROM "stores"
128
134
  // INNER JOIN (
129
135
  // SELECT "store_id" AS "store", COUNT(*) AS "productCount"
130
136
  // FROM "products" GROUP BY "store_id"
131
137
  // ) AS "stats" ON "stores"."id"="stats"."store"
132
138
 
133
139
  // Left join
134
- const stores = await Store.find().leftJoin(productCounts, 'stats', { on: { id: 'store' } });
140
+ const stores = await storeRepository.find().leftJoin(productCounts, 'stats', { on: { id: 'store' } });
135
141
  ```
136
142
 
137
143
  Multiple ON conditions:
138
144
 
139
145
  ```ts
140
- const categoryStats = subquery(Product)
146
+ const categoryStats = subquery(productRepository)
141
147
  .select(['store', 'category', (sb) => sb.count().as('count')])
142
148
  .groupBy(['store', 'category']);
143
149
 
144
- const stores = await Store.find().join(categoryStats, 'stats', { on: { id: 'store', categoryId: 'category' } });
150
+ const stores = await storeRepository.find().join(categoryStats, 'stats', { on: { id: 'store', categoryId: 'category' } });
145
151
  ```
146
152
 
147
153
  ## Sorting on joined columns
@@ -150,10 +156,11 @@ Use dot notation to sort by joined table columns:
150
156
 
151
157
  ```ts
152
158
  // Model join
153
- const products = await Product.find().join('store').sort('store.name asc');
159
+ const products = await productRepository.find().join('store').sort('store.name asc');
154
160
 
155
161
  // Subquery join
156
- const stores = await Store.find()
162
+ const stores = await storeRepository
163
+ .find()
157
164
  .join(productCounts, 'stats', { on: { id: 'store' } })
158
165
  .sort('stats.productCount desc');
159
166
  ```
@@ -173,7 +180,7 @@ Available in subquery selects:
173
180
  | `min(column)` | Minimum value |
174
181
 
175
182
  ```ts
176
- const stats = subquery(Product)
183
+ const stats = subquery(productRepository)
177
184
  .select(['store', (sb) => sb.count().as('totalProducts'), (sb) => sb.sum('price').as('totalValue'), (sb) => sb.avg('price').as('avgPrice'), (sb) => sb.count('name').distinct().as('uniqueNames')])
178
185
  .groupBy(['store']);
179
186
  ```
@@ -183,7 +190,7 @@ If `.as()` is not called, aggregates use their function name as the alias (e.g.
183
190
  ## GROUP BY and HAVING
184
191
 
185
192
  ```ts
186
- const popularCategories = subquery(Product)
193
+ const popularCategories = subquery(productRepository)
187
194
  .select(['category', (sb) => sb.count().as('productCount')])
188
195
  .groupBy(['category'])
189
196
  .having({ productCount: { '>': 10 } });
@@ -215,14 +222,14 @@ Annotate aggregate callbacks with `TypedAggregateExpression` for compile-time co
215
222
  ```ts
216
223
  import type { TypedAggregateExpression } from 'bigal';
217
224
 
218
- const productCounts = subquery(Product)
225
+ const productCounts = subquery(productRepository)
219
226
  .select([
220
227
  'store',
221
228
  (sb): TypedAggregateExpression<'productCount'> => sb.count().as('productCount'),
222
229
  ])
223
230
  .groupBy(['store']);
224
231
 
225
- const stores = await Store.find()
232
+ const stores = await storeRepository.find()
226
233
  .join(productCounts, 'stats', { on: { id: 'store' } })
227
234
  .sort('stats.productCount desc'); // Type-safe!
228
235
 
@@ -235,9 +242,9 @@ const stores = await Store.find()
235
242
  Use `distinctOn()` for "greatest-per-group" patterns:
236
243
 
237
244
  ```ts
238
- const latestProducts = subquery(Product).select(['store', 'name', 'createdAt']).distinctOn(['store']).sort('store').sort('createdAt desc');
245
+ const latestProducts = subquery(productRepository).select(['store', 'name', 'createdAt']).distinctOn(['store']).sort('store').sort('createdAt desc');
239
246
 
240
- const stores = await Store.find().join(latestProducts, 'latestProduct', { on: { id: 'store' } });
247
+ const stores = await storeRepository.find().join(latestProducts, 'latestProduct', { on: { id: 'store' } });
241
248
  ```
242
249
 
243
250
  See [Querying > DISTINCT ON](/guide/querying#distinct-on) for constraints and usage with top-level queries.
@@ -1,28 +1,39 @@
1
1
  ---
2
- description: Define readonly models backed by PostgreSQL views with view() and ReadonlyRepository. Supports schema options and all query features.
2
+ description: Map PostgreSQL views to readonly models with ReadonlyRepository. Supports inheritance, schema options, and all query features.
3
3
  ---
4
4
 
5
5
  # Views and Readonly Repositories
6
6
 
7
- BigAl provides a `view()` function for defining models backed by PostgreSQL views. The `view()`
8
- function is equivalent to `table()` with `readonly: true` - it produces a `ReadonlyRepository`
9
- that omits `create`, `update`, and `destroy` methods, catching accidental writes at compile time.
7
+ BigAl does not distinguish between tables and views. Both use the `@table()` decorator. Setting `readonly: true` causes
8
+ `initialize()` to return a `ReadonlyRepository` which omits `create`, `update`, and `destroy` methods,
9
+ catching accidental writes at compile time.
10
10
 
11
11
  BigAl does not create or manage views. Create them in PostgreSQL via your migration tool.
12
12
 
13
13
  ## Defining a view model
14
14
 
15
- ### Using view()
15
+ ### Standalone model
16
16
 
17
17
  ```ts
18
- import { view, serial, text, integer } from 'bigal';
18
+ import { column, primaryColumn, table, Entity } from 'bigal';
19
19
 
20
- export const ProductSummary = view('product_summaries', {
21
- id: serial().primaryKey(),
22
- name: text().notNull(),
23
- storeName: text().notNull(),
24
- categoryCount: integer().notNull(),
25
- });
20
+ @table({
21
+ name: 'product_summaries',
22
+ readonly: true,
23
+ })
24
+ export class ProductSummary extends Entity {
25
+ @primaryColumn({ type: 'integer' })
26
+ public id!: number;
27
+
28
+ @column({ type: 'string', required: true })
29
+ public name!: string;
30
+
31
+ @column({ type: 'string', required: true, name: 'store_name' })
32
+ public storeName!: string;
33
+
34
+ @column({ type: 'integer', required: true, name: 'category_count' })
35
+ public categoryCount!: number;
36
+ }
26
37
  ```
27
38
 
28
39
  Corresponding view in PostgreSQL:
@@ -40,47 +51,19 @@ LEFT JOIN product_categories pc ON pc.product_id = p.id
40
51
  GROUP BY p.id, p.name, s.name;
41
52
  ```
42
53
 
43
- ### Using table() with readonly option
44
-
45
- Alternatively, use `table()` with `readonly: true`:
46
-
47
- ```ts
48
- import { defineTable as table, serial, text, integer } from 'bigal';
49
-
50
- export const ProductSummary = table(
51
- 'product_summaries',
52
- {
53
- id: serial().primaryKey(),
54
- name: text().notNull(),
55
- storeName: text().notNull(),
56
- categoryCount: integer().notNull(),
57
- },
58
- { readonly: true },
59
- );
60
- ```
61
-
62
- ### Sharing columns with a table
54
+ ### Inheriting from an existing model
63
55
 
64
- If a view returns the same columns as a table (e.g., a filtered subset),
65
- share the column definitions:
56
+ If the view has the same columns as an existing table, extend the model to reuse column definitions:
66
57
 
67
58
  ```ts
68
- import { table, view, serial, text, integer, boolean } from 'bigal';
69
-
70
- const productColumns = {
71
- id: serial().primaryKey(),
72
- name: text().notNull(),
73
- priceCents: integer().notNull(),
74
- isActive: boolean().notNull(),
75
- };
76
-
77
- export const Product = table('products', {
78
- ...productColumns,
79
- store: belongsTo('Store'),
80
- });
81
-
82
- // View returns the same columns, just filtered to active products
83
- export const ActiveProduct = view('active_products', productColumns);
59
+ import { table } from 'bigal';
60
+ import { Product } from './Product';
61
+
62
+ @table({
63
+ name: 'readonly_products',
64
+ readonly: true,
65
+ })
66
+ export class ReadonlyProduct extends Product {}
84
67
  ```
85
68
 
86
69
  ### Schema option
@@ -88,42 +71,31 @@ export const ActiveProduct = view('active_products', productColumns);
88
71
  For views in a non-default schema:
89
72
 
90
73
  ```ts
91
- export const ReportSummary = view(
92
- 'product_summaries',
93
- {
94
- id: serial().primaryKey(),
95
- storeName: text().notNull(),
96
- totalRevenue: integer().notNull(),
97
- },
98
- { schema: 'reporting' },
99
- );
74
+ @table({
75
+ schema: 'reporting',
76
+ name: 'product_summaries',
77
+ readonly: true,
78
+ })
79
+ export class ProductSummary extends Entity {
80
+ // ...
81
+ }
100
82
  ```
101
83
 
102
84
  ## Initializing the repository
103
85
 
104
- Include the model in `initialize()`. BigAl creates a `ReadonlyRepository` automatically for models
105
- defined with `view()` or `table()` with `readonly: true`:
86
+ Include the view model in `initialize()`. BigAl creates a `ReadonlyRepository` automatically:
106
87
 
107
88
  ```ts
108
- import { initialize } from 'bigal';
109
- import type { ReadonlyRepository } from 'bigal';
89
+ import { initialize, ReadonlyRepository } from 'bigal';
110
90
  import { Product, Store, ProductSummary } from './models';
111
91
 
112
- const { Product, ProductSummary } = initialize({
113
- models: { Product, Store, ProductSummary },
92
+ const repos = initialize({
93
+ models: [Product, Store, ProductSummary],
114
94
  pool,
115
95
  readonlyPool,
116
96
  });
117
97
 
118
- // ProductSummary only has find(), findOne(), and count()
119
- ```
120
-
121
- For repository type annotations, use `ReadonlyRepository<typeof Model>`:
122
-
123
- ```ts
124
- function getSummaries(repo: ReadonlyRepository<typeof ProductSummary>) {
125
- return repo.find().where({ categoryCount: { '>': 5 } });
126
- }
98
+ const productSummaryRepository = repos.ProductSummary as ReadonlyRepository<ProductSummary>;
127
99
  ```
128
100
 
129
101
  ## Querying
@@ -131,18 +103,18 @@ function getSummaries(repo: ReadonlyRepository<typeof ProductSummary>) {
131
103
  Readonly repositories support the same query methods as regular repositories:
132
104
 
133
105
  ```ts
134
- const summaries = await ProductSummary.find()
106
+ const summaries = await productSummaryRepository
107
+ .find()
135
108
  .where({ storeName: { contains: 'Acme' } })
136
109
  .sort('categoryCount desc')
137
110
  .limit(10);
138
111
 
139
- const summary = await ProductSummary.findOne().where({ id: 42 });
112
+ const summary = await productSummaryRepository.findOne().where({ id: 42 });
140
113
 
141
- const count = await ProductSummary.count().where({ categoryCount: { '>': 5 } });
114
+ const count = await productSummaryRepository.count().where({ categoryCount: { '>': 5 } });
142
115
  ```
143
116
 
144
- All query features work: `where` operators, `sort`, `skip`, `limit`, `paginate`, `populate`, `join`,
145
- `withCount`, and `distinctOn`.
117
+ All query features work: `where` operators, `sort`, `skip`, `limit`, `paginate`, `populate`, `join`, `withCount`, and `distinctOn`.
146
118
 
147
119
  ## Available methods
148
120
 
package/docs/index.md CHANGED
@@ -2,7 +2,7 @@
2
2
  layout: home
3
3
  hero:
4
4
  text: PostgreSQL-optimized TypeScript ORM
5
- tagline: Built exclusively for Postgres. Type-safe fluent query builder, function-based models, and queries tuned for Postgres performance.
5
+ tagline: Built exclusively for Postgres. Type-safe fluent query builder, decorator-based models, and queries tuned for Postgres performance.
6
6
  actions:
7
7
  - theme: brand
8
8
  text: Get Started
@@ -15,10 +15,10 @@ features:
15
15
  details: Built exclusively for Postgres, not a lowest-common-denominator abstraction. Queries are tuned for Postgres performance. JSONB, DISTINCT ON, subquery joins, and ON CONFLICT upserts work out of the box.
16
16
  - title: Fluent builder pattern
17
17
  details: Chain .where(), .sort(), .limit(), .join(), and .populate() calls. Each method returns a new immutable instance.
18
- - title: Function-based models
19
- details: Define models with table() and view(), PostgreSQL-native column builders, and relationship helpers. Types are inferred from the schema definition.
18
+ - title: Decorator-based models
19
+ details: Define tables, columns, and relationships with TypeScript decorators. Inheritance and schema support built in.
20
20
  - title: Type-safe queries
21
- details: WhereQuery types narrow automatically - relationship fields resolve to foreign keys or populated entities without type assertions.
21
+ details: WhereQuery types narrow automatically relationship fields resolve to foreign keys or populated entities without type assertions.
22
22
  ---
23
23
 
24
24
  <!-- markdownlint-disable MD013 MD033 MD041 -->
@@ -48,47 +48,51 @@ features:
48
48
  <div class="code-showcase">
49
49
 
50
50
  <h2>What it looks like</h2>
51
- <p class="subtitle">Define a model, query it - that's it.</p>
51
+ <p class="subtitle">Define a model, query it that's it.</p>
52
52
 
53
53
  ```ts
54
- // models/Product.ts
55
- import { defineTable as table, serial, text, integer, belongsTo } from 'bigal';
56
-
57
- export const Product = table('products', {
58
- id: serial().primaryKey(),
59
- name: text().notNull(),
60
- priceCents: integer().notNull(),
61
- sku: text(),
62
- store: belongsTo('Store'),
63
- });
64
- ```
65
-
66
- ```ts
67
- // app.ts
68
- import { initialize, subquery } from 'bigal';
54
+ import { column, primaryColumn, table, Entity, initialize } from 'bigal';
69
55
  import { Pool } from 'postgres-pool';
70
56
 
71
- const { Product, Store } = initialize({
72
- models: { Product, Store },
57
+ @table({ name: 'products' })
58
+ class Product extends Entity {
59
+ @primaryColumn({ type: 'integer' })
60
+ public id!: number;
61
+
62
+ @column({ type: 'string', required: true })
63
+ public name!: string;
64
+
65
+ @column({ type: 'integer', required: true })
66
+ public priceCents!: number;
67
+
68
+ @column({ model: () => 'Store', name: 'store_id' })
69
+ public store!: number | Store;
70
+ }
71
+
72
+ const repos = initialize({
73
+ models: [Product, Store],
73
74
  pool: new Pool('postgres://localhost/mydb'),
74
75
  });
76
+ const productRepo = repos.Product as Repository<Product>;
75
77
 
76
- // Fluent queries - just await the chain
77
- const products = await Product.find()
78
+ // Fluent queries just await the chain
79
+ const products = await productRepo
80
+ .find()
78
81
  .where({ priceCents: { '>': 1000 }, name: { contains: 'widget' } })
79
82
  .sort('name asc')
80
83
  .limit(10);
81
84
 
82
85
  // Joins and subqueries
83
- const expensiveProducts = await Product.find()
86
+ const expensiveProducts = await productRepo
87
+ .find()
84
88
  .join('store')
85
89
  .where({
86
90
  store: { name: 'Acme' },
87
- priceCents: { '>': subquery(Product).avg('priceCents') },
91
+ price: { '>': subquery(productRepo).avg('price') },
88
92
  });
89
93
 
90
94
  // Upserts with ON CONFLICT
91
- await Product.create({ name: 'Widget', sku: 'WDG-001', priceCents: 999 }, { onConflict: { action: 'merge', targets: ['sku'], merge: ['priceCents'] } });
95
+ await productRepo.create({ name: 'Widget', sku: 'WDG-001', priceCents: 999 }, { onConflict: { action: 'merge', targets: ['sku'], merge: ['priceCents'] } });
92
96
  ```
93
97
 
94
98
  </div>
package/docs/package.json CHANGED
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "vitepress": "1.6.4",
12
- "vitepress-plugin-llms": "1.12.0"
13
- }
12
+ "vitepress-plugin-llms": "1.13.2"
13
+ },
14
+ "packageManager": "pnpm@11.11.0+sha512.4463f65fd80ed80d69bc1d4bf163ee94f605c7380fc318bb5b2ebe15f8cd12d49c51a4d59e951b401e764d3b6ca751cbf51bc50ed7001a6bcb4935e684c34882"
14
15
  }