bigal 16.0.0-beta.3 → 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.
- package/.devcontainer/devcontainer.json +1 -1
- package/.vite-hooks/pre-commit +1 -0
- package/CHANGELOG.md +25 -11
- package/CLAUDE.md +4 -4
- package/README.md +29 -43
- package/dist/index.cjs +3274 -4277
- package/dist/index.d.cts +1511 -1425
- package/dist/index.d.mts +1511 -1425
- package/dist/index.mjs +3265 -4244
- package/docs/.vitepress/config.ts +0 -1
- package/docs/advanced/bigal-vs-raw-sql.md +23 -23
- package/docs/advanced/known-issues.md +26 -21
- package/docs/getting-started.md +35 -29
- package/docs/guide/crud-operations.md +42 -61
- package/docs/guide/models.md +97 -396
- package/docs/guide/querying.md +82 -98
- package/docs/guide/relationships.md +125 -90
- package/docs/guide/subqueries-and-joins.md +39 -32
- package/docs/guide/views.md +51 -79
- package/docs/index.md +31 -27
- package/docs/package.json +3 -2
- package/docs/pnpm-lock.yaml +2338 -0
- package/docs/pnpm-workspace.yaml +3 -0
- package/docs/reference/api.md +86 -383
- package/docs/reference/configuration.md +26 -113
- package/package.json +59 -78
- package/pnpm-workspace.yaml +14 -0
- package/release.config.mjs +1 -1
- package/skills/using-bigal/SKILL.md +213 -276
- package/vite.config.ts +83 -0
- package/.husky/pre-commit +0 -4
- package/.oxfmtrc.json +0 -38
- package/.oxlintrc.json +0 -219
- package/dist/index.d.ts +0 -1527
- package/docs/guide/migration-v16.md +0 -594
- package/docs/package-lock.json +0 -3753
- package/scripts/migrate-v16.ts +0 -578
- package/skills/upgrade-v16/SKILL.md +0 -296
|
@@ -13,7 +13,7 @@ Use the `subquery()` function:
|
|
|
13
13
|
```ts
|
|
14
14
|
import { subquery } from 'bigal';
|
|
15
15
|
|
|
16
|
-
const activeStores = subquery(
|
|
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(
|
|
34
|
+
const activeStores = subquery(storeRepository).select(['id']).where({ isActive: true });
|
|
35
35
|
|
|
36
36
|
// WHERE IN
|
|
37
|
-
const products = await
|
|
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
|
|
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(
|
|
51
|
+
const hasProducts = subquery(productRepository).where({ name: { like: 'Widget%' } });
|
|
52
52
|
|
|
53
53
|
// EXISTS
|
|
54
|
-
const stores = await
|
|
54
|
+
const stores = await storeRepository.find().where({
|
|
55
55
|
exists: hasProducts,
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
// NOT EXISTS
|
|
59
|
-
const stores = await
|
|
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(
|
|
71
|
+
const avgPrice = subquery(productRepository).avg('price');
|
|
72
72
|
|
|
73
|
-
const expensiveProducts = await
|
|
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(
|
|
83
|
+
.where({ price: subquery(productRepository).max('price') })
|
|
84
84
|
|
|
85
85
|
// Not equal to min price
|
|
86
|
-
.where({ price: { '!': subquery(
|
|
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
|
|
95
|
+
const products = await productRepository
|
|
96
|
+
.find()
|
|
96
97
|
.join('store')
|
|
97
98
|
.where({ store: { name: 'Acme' } });
|
|
98
|
-
// SQL: SELECT "products"
|
|
99
|
-
// INNER JOIN "stores" ON "products"."store_id"="
|
|
100
|
-
// WHERE "
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
127
|
-
// SQL: SELECT "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
|
|
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(
|
|
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
|
|
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
|
|
159
|
+
const products = await productRepository.find().join('store').sort('store.name asc');
|
|
154
160
|
|
|
155
161
|
// Subquery join
|
|
156
|
-
const stores = await
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
245
|
+
const latestProducts = subquery(productRepository).select(['store', 'name', 'createdAt']).distinctOn(['store']).sort('store').sort('createdAt desc');
|
|
239
246
|
|
|
240
|
-
const stores = await
|
|
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.
|
package/docs/guide/views.md
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
###
|
|
15
|
+
### Standalone model
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import {
|
|
18
|
+
import { column, primaryColumn, table, Entity } from 'bigal';
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
###
|
|
44
|
-
|
|
45
|
-
Alternatively, use `table()` with `readonly: true`:
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
import { 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
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
name:
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
92
|
-
'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
|
113
|
-
models:
|
|
92
|
+
const repos = initialize({
|
|
93
|
+
models: [Product, Store, ProductSummary],
|
|
114
94
|
pool,
|
|
115
95
|
readonlyPool,
|
|
116
96
|
});
|
|
117
97
|
|
|
118
|
-
|
|
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
|
|
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
|
|
112
|
+
const summary = await productSummaryRepository.findOne().where({ id: 42 });
|
|
140
113
|
|
|
141
|
-
const count = await
|
|
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,
|
|
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:
|
|
19
|
-
details: Define
|
|
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
|
|
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
|
|
51
|
+
<p class="subtitle">Define a model, query it — that's it.</p>
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
|
-
|
|
55
|
-
import { 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
|
-
|
|
72
|
-
|
|
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
|
|
77
|
-
const products = await
|
|
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
|
|
86
|
+
const expensiveProducts = await productRepo
|
|
87
|
+
.find()
|
|
84
88
|
.join('store')
|
|
85
89
|
.where({
|
|
86
90
|
store: { name: 'Acme' },
|
|
87
|
-
|
|
91
|
+
price: { '>': subquery(productRepo).avg('price') },
|
|
88
92
|
});
|
|
89
93
|
|
|
90
94
|
// Upserts with ON CONFLICT
|
|
91
|
-
await
|
|
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.
|
|
13
|
-
}
|
|
12
|
+
"vitepress-plugin-llms": "1.13.2"
|
|
13
|
+
},
|
|
14
|
+
"packageManager": "pnpm@11.11.0+sha512.4463f65fd80ed80d69bc1d4bf163ee94f605c7380fc318bb5b2ebe15f8cd12d49c51a4d59e951b401e764d3b6ca751cbf51bc50ed7001a6bcb4935e684c34882"
|
|
14
15
|
}
|