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.
- package/.devcontainer/devcontainer.json +1 -1
- package/.vite-hooks/pre-commit +1 -0
- package/CHANGELOG.md +28 -8
- package/CLAUDE.md +4 -4
- package/README.md +29 -43
- package/dist/index.cjs +3275 -4259
- package/dist/index.d.cts +1511 -1424
- package/dist/index.d.mts +1511 -1424
- package/dist/index.mjs +3265 -4225
- 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 -83
- 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 -384
- package/docs/reference/configuration.md +26 -113
- package/package.json +59 -73
- 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 -1526
- package/docs/guide/migration-v16.md +0 -594
- package/docs/package-lock.json +0 -3753
- package/scripts/migrate-v16.ts +0 -497
- package/skills/upgrade-v16/SKILL.md +0 -244
package/docs/guide/querying.md
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Fluent query builder for find, findOne, and count with WHERE operators, JSONB querying, pagination, sorting,
|
|
2
|
+
description: Fluent query builder for find, findOne, and count with WHERE operators, JSONB querying, pagination, sorting, DISTINCT ON, and populate.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Querying
|
|
6
6
|
|
|
7
|
-
BigAl provides `findOne()`, `find()`, and `count()` methods on repositories. Queries use a fluent
|
|
8
|
-
|
|
9
|
-
can `await` them directly.
|
|
7
|
+
BigAl provides `findOne()`, `find()`, and `count()` methods on repositories. Queries use a fluent builder pattern —
|
|
8
|
+
each method returns a new immutable instance, and queries are `PromiseLike` so you can `await` them directly.
|
|
10
9
|
|
|
11
10
|
## findOne
|
|
12
11
|
|
|
13
12
|
Returns a single record or `null`:
|
|
14
13
|
|
|
15
14
|
```ts
|
|
16
|
-
const product = await
|
|
15
|
+
const product = await productRepository.findOne().where({ id: 42 });
|
|
17
16
|
```
|
|
18
17
|
|
|
19
18
|
### Query projection
|
|
@@ -21,9 +20,11 @@ const product = await Product.findOne().where({ id: 42 });
|
|
|
21
20
|
Select specific columns:
|
|
22
21
|
|
|
23
22
|
```ts
|
|
24
|
-
const product = await
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const product = await productRepository
|
|
24
|
+
.findOne({
|
|
25
|
+
select: ['name', 'sku'],
|
|
26
|
+
})
|
|
27
|
+
.where({ id: 42 });
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
### Pool override
|
|
@@ -31,9 +32,11 @@ const product = await Product.findOne({
|
|
|
31
32
|
Use an explicit connection pool:
|
|
32
33
|
|
|
33
34
|
```ts
|
|
34
|
-
const product = await
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const product = await productRepository
|
|
36
|
+
.findOne({
|
|
37
|
+
pool: poolOverride,
|
|
38
|
+
})
|
|
39
|
+
.where({ id: 42 });
|
|
37
40
|
```
|
|
38
41
|
|
|
39
42
|
## find
|
|
@@ -41,7 +44,7 @@ const product = await Product.findOne({
|
|
|
41
44
|
Returns an array of records:
|
|
42
45
|
|
|
43
46
|
```ts
|
|
44
|
-
const products = await
|
|
47
|
+
const products = await productRepository.find().where({ store: storeId });
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
## count
|
|
@@ -49,11 +52,17 @@ const products = await Product.find().where({ store: storeId });
|
|
|
49
52
|
Returns the number of matching records:
|
|
50
53
|
|
|
51
54
|
```ts
|
|
52
|
-
const count = await
|
|
55
|
+
const count = await productRepository.count().where({
|
|
53
56
|
name: { like: 'Widget%' },
|
|
54
57
|
});
|
|
55
58
|
```
|
|
56
59
|
|
|
60
|
+
If you only need to know whether a match exists, use `count()` instead of `findOne()` — it performs better since it doesn't select or hydrate a row:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const exists = (await productRepository.count().where({ sku: 'ABC123' })) > 0;
|
|
64
|
+
```
|
|
65
|
+
|
|
57
66
|
## Where operators
|
|
58
67
|
|
|
59
68
|
### String matching
|
|
@@ -68,10 +77,10 @@ All string operators use case-insensitive matching (`ILIKE`) and accept arrays f
|
|
|
68
77
|
| `endsWith` | Suffix match | `%value` |
|
|
69
78
|
|
|
70
79
|
```ts
|
|
71
|
-
await
|
|
80
|
+
await productRepository.find().where({ name: { contains: 'widget' } });
|
|
72
81
|
// SQL: WHERE name ILIKE '%widget%'
|
|
73
82
|
|
|
74
|
-
await
|
|
83
|
+
await productRepository.find().where({ name: { startsWith: 'Pro' } });
|
|
75
84
|
// SQL: WHERE name ILIKE 'Pro%'
|
|
76
85
|
```
|
|
77
86
|
|
|
@@ -85,10 +94,10 @@ await Product.find().where({ name: { startsWith: 'Pro' } });
|
|
|
85
94
|
| `>=` | Greater than or equal |
|
|
86
95
|
|
|
87
96
|
```ts
|
|
88
|
-
await
|
|
97
|
+
await productRepository.find().where({ price: { '>=': 100 } });
|
|
89
98
|
|
|
90
99
|
// Multiple operators on same field (AND)
|
|
91
|
-
await
|
|
100
|
+
await productRepository.find().where({
|
|
92
101
|
createdAt: { '>=': startDate, '<': endDate },
|
|
93
102
|
});
|
|
94
103
|
```
|
|
@@ -103,13 +112,13 @@ await personRepository.find().where({ age: [22, 23, 24] });
|
|
|
103
112
|
### Negation (`!`)
|
|
104
113
|
|
|
105
114
|
```ts
|
|
106
|
-
await
|
|
115
|
+
await productRepository.find().where({ status: { '!': 'discontinued' } });
|
|
107
116
|
// SQL: WHERE status <> $1
|
|
108
117
|
|
|
109
|
-
await
|
|
118
|
+
await productRepository.find().where({ status: { '!': ['a', 'b'] } });
|
|
110
119
|
// SQL: WHERE status NOT IN ($1, $2)
|
|
111
120
|
|
|
112
|
-
await
|
|
121
|
+
await productRepository.find().where({ deletedAt: { '!': null } });
|
|
113
122
|
// SQL: WHERE deleted_at IS NOT NULL
|
|
114
123
|
```
|
|
115
124
|
|
|
@@ -122,7 +131,7 @@ await personRepository.find().where({
|
|
|
122
131
|
// SQL: WHERE (first_name = $1) OR (last_name = $2)
|
|
123
132
|
```
|
|
124
133
|
|
|
125
|
-
###
|
|
134
|
+
### AND with nested OR
|
|
126
135
|
|
|
127
136
|
```ts
|
|
128
137
|
await personRepository.find().where({
|
|
@@ -177,13 +186,11 @@ await repo.find().where({ bar: { theme: { '!': null } } });
|
|
|
177
186
|
// SQL: WHERE "bar"->>'theme' IS NOT NULL
|
|
178
187
|
```
|
|
179
188
|
|
|
180
|
-
Note that `IS NULL` on a JSONB property is true both when the key is missing from the object and when
|
|
181
|
-
|
|
182
|
-
`NULL` in both cases.
|
|
189
|
+
Note that `IS NULL` on a JSONB property is true both when the key is missing from the object and when it is
|
|
190
|
+
explicitly set to `null`. This matches PostgreSQL's behavior — the `->>` operator returns `NULL` in both cases.
|
|
183
191
|
|
|
184
|
-
Properties set to `undefined` in a where clause are silently ignored (standard JavaScript
|
|
185
|
-
|
|
186
|
-
use `null` explicitly.
|
|
192
|
+
Properties set to `undefined` in a where clause are silently ignored (standard JavaScript — `undefined` values are
|
|
193
|
+
dropped by `Object.entries`). To query for missing or null properties, always use `null` explicitly.
|
|
187
194
|
|
|
188
195
|
### JSONB containment
|
|
189
196
|
|
|
@@ -201,49 +208,51 @@ await repo.find().where({
|
|
|
201
208
|
### String syntax
|
|
202
209
|
|
|
203
210
|
```ts
|
|
204
|
-
await
|
|
205
|
-
await
|
|
211
|
+
await productRepository.find().where({}).sort('name asc');
|
|
212
|
+
await productRepository.find().where({}).sort('name asc, createdAt desc');
|
|
206
213
|
```
|
|
207
214
|
|
|
208
215
|
### Object syntax
|
|
209
216
|
|
|
210
217
|
```ts
|
|
211
|
-
await
|
|
212
|
-
await
|
|
218
|
+
await productRepository.find().where({}).sort({ name: 1 }); // ASC
|
|
219
|
+
await productRepository.find().where({}).sort({ name: 1, createdAt: -1 }); // ASC, DESC
|
|
213
220
|
```
|
|
214
221
|
|
|
215
222
|
## Vector distance queries
|
|
216
223
|
|
|
217
|
-
BigAl supports nearest-neighbor queries on
|
|
218
|
-
|
|
224
|
+
BigAl supports nearest-neighbor queries on columns declared with `@column({ type: 'vector', dimensions: n })`,
|
|
225
|
+
backed by the [pgvector](https://github.com/pgvector/pgvector) extension. Four distance metrics are
|
|
226
|
+
available: `cosine`, `l2`, `l1`, and `innerProduct`. The `l1` metric requires pgvector >= 0.7.0.
|
|
227
|
+
|
|
228
|
+
| Metric | PostgreSQL operator | Description |
|
|
229
|
+
| -------------- | ------------------- | ------------------------- |
|
|
230
|
+
| `cosine` | `<=>` | Cosine distance (default) |
|
|
231
|
+
| `l2` | `<->` | Euclidean distance |
|
|
232
|
+
| `l1` | `<+>` | Manhattan distance |
|
|
233
|
+
| `innerProduct` | `<#>` | Negative inner product |
|
|
219
234
|
|
|
220
235
|
### Sorting by distance
|
|
221
236
|
|
|
222
237
|
Use the `nearestTo` sort to order results by vector similarity:
|
|
223
238
|
|
|
224
239
|
```ts
|
|
225
|
-
const similar = await
|
|
240
|
+
const similar = await documentRepository
|
|
226
241
|
.find()
|
|
227
|
-
.where({})
|
|
242
|
+
.where({ title: { contains: 'biology' } })
|
|
228
243
|
.sort({ embedding: { nearestTo: queryVector, metric: 'cosine' } })
|
|
229
244
|
.limit(10);
|
|
245
|
+
// SQL: ... WHERE "title" ILIKE $1 ORDER BY "embedding" <=> $2 LIMIT 10
|
|
230
246
|
```
|
|
231
247
|
|
|
232
|
-
The `metric` option defaults to `'cosine'` if omitted.
|
|
233
|
-
|
|
234
|
-
| Metric | PostgreSQL operator | Description |
|
|
235
|
-
| -------------- | ------------------- | ------------------------- |
|
|
236
|
-
| `cosine` | `<=>` | Cosine distance (default) |
|
|
237
|
-
| `l2` | `<->` | Euclidean distance |
|
|
238
|
-
| `l1` | `<+>` | Manhattan distance |
|
|
239
|
-
| `innerProduct` | `<#>` | Negative inner product |
|
|
248
|
+
The `metric` option defaults to `'cosine'` if omitted. An unknown metric throws a `QueryError`.
|
|
240
249
|
|
|
241
250
|
### Filtering by distance
|
|
242
251
|
|
|
243
252
|
Combine `nearestTo` in the where clause with a distance threshold:
|
|
244
253
|
|
|
245
254
|
```ts
|
|
246
|
-
const nearby = await
|
|
255
|
+
const nearby = await documentRepository
|
|
247
256
|
.find()
|
|
248
257
|
.where({
|
|
249
258
|
embedding: {
|
|
@@ -254,6 +263,21 @@ const nearby = await documentRepo
|
|
|
254
263
|
})
|
|
255
264
|
.sort({ embedding: { nearestTo: queryVector, metric: 'cosine' } })
|
|
256
265
|
.limit(10);
|
|
266
|
+
// SQL: ... WHERE "embedding" <=> $1 < $2 ORDER BY "embedding" <=> $3 LIMIT 10
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
At least one `distance` bound is required in where clauses; multiple bounds are combined with `AND`
|
|
270
|
+
(for example `distance: { '>': 0.1, '<': 0.5 }` finds a distance band). Vectors must be non-empty
|
|
271
|
+
arrays of finite numbers.
|
|
272
|
+
|
|
273
|
+
### Equality and writes
|
|
274
|
+
|
|
275
|
+
Vector values round-trip as `number[]`. Where clauses compare whole vectors, and create/update
|
|
276
|
+
serialize the array to pgvector's text format:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
await documentRepository.create({ title: 'foo', embedding: [0.1, 0.2, 0.3] }); // Sends '[0.1,0.2,0.3]'
|
|
280
|
+
await documentRepository.findOne({ embedding: queryVector }); // WHERE "embedding"=$1
|
|
257
281
|
```
|
|
258
282
|
|
|
259
283
|
## Pagination
|
|
@@ -261,7 +285,7 @@ const nearby = await documentRepo
|
|
|
261
285
|
### skip and limit
|
|
262
286
|
|
|
263
287
|
```ts
|
|
264
|
-
await
|
|
288
|
+
await productRepository.find().where({}).skip(20).limit(10);
|
|
265
289
|
```
|
|
266
290
|
|
|
267
291
|
### paginate
|
|
@@ -269,7 +293,7 @@ await Product.find().where({}).skip(20).limit(10);
|
|
|
269
293
|
```ts
|
|
270
294
|
const page = 2;
|
|
271
295
|
const pageSize = 25;
|
|
272
|
-
await
|
|
296
|
+
await productRepository.find().where({}).paginate(page, pageSize);
|
|
273
297
|
```
|
|
274
298
|
|
|
275
299
|
### withCount
|
|
@@ -277,7 +301,7 @@ await Product.find().where({}).paginate(page, pageSize);
|
|
|
277
301
|
Get paginated results with total count in a single query using `COUNT(*) OVER()`:
|
|
278
302
|
|
|
279
303
|
```ts
|
|
280
|
-
const { results, totalCount } = await
|
|
304
|
+
const { results, totalCount } = await productRepository.find().where({ store: storeId }).sort('name').limit(10).skip(20).withCount();
|
|
281
305
|
|
|
282
306
|
const totalPages = Math.ceil(totalCount / 10);
|
|
283
307
|
```
|
|
@@ -288,7 +312,7 @@ PostgreSQL's `DISTINCT ON` returns one row per unique combination of columns:
|
|
|
288
312
|
|
|
289
313
|
```ts
|
|
290
314
|
// Most recently created product per store
|
|
291
|
-
const latest = await
|
|
315
|
+
const latest = await productRepository.find().distinctOn(['store']).sort('store').sort('createdAt desc');
|
|
292
316
|
```
|
|
293
317
|
|
|
294
318
|
Requirements:
|
|
@@ -296,49 +320,24 @@ Requirements:
|
|
|
296
320
|
- `ORDER BY` is required and must start with the `DISTINCT ON` columns
|
|
297
321
|
- Cannot be combined with `withCount()`
|
|
298
322
|
|
|
299
|
-
## Global filters
|
|
300
|
-
|
|
301
|
-
When a table defines named filters (see [Models > Global filters](/guide/models#global-filters)),
|
|
302
|
-
they are automatically applied to every `find` and `findOne` query. Override them per query:
|
|
303
|
-
|
|
304
|
-
```ts
|
|
305
|
-
// Disable all filters for this query
|
|
306
|
-
await Product.find().where({}).filters(false);
|
|
307
|
-
|
|
308
|
-
// Disable a specific filter
|
|
309
|
-
await Product.find().where({}).filters({ active: false });
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
Filters are not applied to `count()` queries.
|
|
313
|
-
|
|
314
|
-
## toSQL()
|
|
315
|
-
|
|
316
|
-
Inspect the generated SQL and parameters without executing the query:
|
|
317
|
-
|
|
318
|
-
```ts
|
|
319
|
-
const { sql, params } = Product.find()
|
|
320
|
-
.where({ name: { contains: 'widget' } })
|
|
321
|
-
.sort('name')
|
|
322
|
-
.toSQL();
|
|
323
|
-
|
|
324
|
-
console.log(sql); // SELECT ... FROM "products" WHERE "name" ILIKE $1 ORDER BY ...
|
|
325
|
-
console.log(params); // ['%widget%']
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Available on `find`, `findOne`, `create`, `update`, and `destroy`. Useful for debugging, logging,
|
|
329
|
-
and testing SQL generation.
|
|
330
|
-
|
|
331
323
|
## Populate
|
|
332
324
|
|
|
333
325
|
Load related entities:
|
|
334
326
|
|
|
335
327
|
```ts
|
|
336
|
-
const product = await
|
|
328
|
+
const product = await productRepository
|
|
329
|
+
.findOne()
|
|
337
330
|
.where({ id: 42 })
|
|
338
331
|
.populate('store', { select: ['name'] });
|
|
339
332
|
|
|
340
|
-
// product.store is the full Store entity
|
|
333
|
+
// product.store is the full Store entity
|
|
341
334
|
console.log(product.store.name);
|
|
342
335
|
```
|
|
343
336
|
|
|
344
|
-
|
|
337
|
+
## toJSON
|
|
338
|
+
|
|
339
|
+
Return plain objects without class prototypes:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
const product = await productRepository.findOne().where({ id: 42 }).toJSON();
|
|
343
|
+
```
|
|
@@ -1,154 +1,188 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Many-to-one, one-to-many, and many-to-many relationships with
|
|
2
|
+
description: Many-to-one, one-to-many, and many-to-many relationships with decorators, QueryResult type narrowing, and populate options.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Relationships
|
|
6
6
|
|
|
7
|
-
BigAl supports three relationship patterns: many-to-one, one-to-many, and many-to-many.
|
|
8
|
-
use string model name references by default. Model names are auto-derived from table names (e.g.,
|
|
9
|
-
`products` becomes `Product`).
|
|
7
|
+
BigAl supports three relationship patterns via the `@column` decorator: many-to-one, one-to-many, and many-to-many.
|
|
10
8
|
|
|
11
|
-
## Many-to-one (
|
|
9
|
+
## Many-to-one (model)
|
|
12
10
|
|
|
13
|
-
Use `
|
|
11
|
+
Use `model` when the current entity holds the foreign key:
|
|
14
12
|
|
|
15
13
|
```ts
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
14
|
+
import { column, Entity, primaryColumn, table } from 'bigal';
|
|
15
|
+
import type { Store } from './Store';
|
|
16
|
+
|
|
17
|
+
@table({ name: 'products' })
|
|
18
|
+
export class Product extends Entity {
|
|
19
|
+
@primaryColumn({ type: 'integer' })
|
|
20
|
+
public id!: number;
|
|
21
|
+
|
|
22
|
+
@column({ type: 'string', required: true })
|
|
23
|
+
public name!: string;
|
|
24
|
+
|
|
25
|
+
@column({ model: () => 'Store', name: 'store_id' })
|
|
26
|
+
public store!: number | Store;
|
|
27
|
+
}
|
|
25
28
|
```
|
|
26
29
|
|
|
27
|
-
- The
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
30
|
+
- The property type is `number | Store` — foreign key when not populated, full entity after `.populate()`
|
|
31
|
+
- Use `name: 'store_id'` when the database column differs from the property name
|
|
32
|
+
- Reference the model by string name (`'Store'`) to avoid circular imports
|
|
33
|
+
- Model names are case-insensitive
|
|
31
34
|
|
|
32
|
-
## One-to-many (
|
|
35
|
+
## One-to-many (collection)
|
|
33
36
|
|
|
34
|
-
Use `
|
|
37
|
+
Use `collection` on the inverse side:
|
|
35
38
|
|
|
36
39
|
```ts
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
})
|
|
40
|
+
import { column, Entity, primaryColumn, table } from 'bigal';
|
|
41
|
+
import type { Product } from './Product';
|
|
42
|
+
|
|
43
|
+
@table({ name: 'stores' })
|
|
44
|
+
export class Store extends Entity {
|
|
45
|
+
@primaryColumn({ type: 'integer' })
|
|
46
|
+
public id!: number;
|
|
47
|
+
|
|
48
|
+
@column({ type: 'string' })
|
|
49
|
+
public name?: string;
|
|
50
|
+
|
|
51
|
+
@column({ collection: () => 'Product', via: 'store' })
|
|
52
|
+
public products?: Product[];
|
|
53
|
+
}
|
|
46
54
|
```
|
|
47
55
|
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
populate. `QueryResult` strips them from results, so they only appear after `.populate()`
|
|
56
|
+
- `via` references the property name on the related model (not the database column)
|
|
57
|
+
- Collections **must** be optional (`?`) — they are only present after `.populate()`
|
|
51
58
|
|
|
52
59
|
## Many-to-many (through)
|
|
53
60
|
|
|
54
|
-
Use
|
|
61
|
+
Use `through` for relationships that require a join table:
|
|
55
62
|
|
|
56
63
|
```ts
|
|
57
64
|
// Product.ts
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
@table({ name: 'products' })
|
|
66
|
+
export class Product extends Entity {
|
|
67
|
+
@primaryColumn({ type: 'integer' })
|
|
68
|
+
public id!: number;
|
|
69
|
+
|
|
70
|
+
@column({ type: 'string', required: true })
|
|
71
|
+
public name!: string;
|
|
72
|
+
|
|
73
|
+
@column({
|
|
74
|
+
collection: () => 'Category',
|
|
75
|
+
through: () => 'ProductCategory',
|
|
76
|
+
via: 'product',
|
|
77
|
+
})
|
|
78
|
+
public categories?: Category[];
|
|
79
|
+
}
|
|
63
80
|
```
|
|
64
81
|
|
|
65
82
|
```ts
|
|
66
83
|
// Category.ts
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
84
|
+
@table({ name: 'categories' })
|
|
85
|
+
export class Category extends Entity {
|
|
86
|
+
@primaryColumn({ type: 'integer' })
|
|
87
|
+
public id!: number;
|
|
88
|
+
|
|
89
|
+
@column({ type: 'string', required: true })
|
|
90
|
+
public name!: string;
|
|
91
|
+
|
|
92
|
+
@column({
|
|
93
|
+
collection: () => 'Product',
|
|
94
|
+
through: () => 'ProductCategory',
|
|
95
|
+
via: 'category',
|
|
96
|
+
})
|
|
97
|
+
public products?: Product[];
|
|
98
|
+
}
|
|
72
99
|
```
|
|
73
100
|
|
|
74
101
|
```ts
|
|
75
|
-
// ProductCategory.ts (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
// ProductCategory.ts (join table)
|
|
103
|
+
@table({ name: 'product__category' })
|
|
104
|
+
export class ProductCategory extends Entity {
|
|
105
|
+
@primaryColumn({ type: 'integer' })
|
|
106
|
+
public id!: number;
|
|
107
|
+
|
|
108
|
+
@column({ model: () => 'Product', name: 'product_id' })
|
|
109
|
+
public product!: number | Product;
|
|
110
|
+
|
|
111
|
+
@column({ model: () => 'Category', name: 'category_id' })
|
|
112
|
+
public category!: number | Category;
|
|
113
|
+
}
|
|
83
114
|
```
|
|
84
115
|
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
- The
|
|
116
|
+
- `through` specifies the join table model
|
|
117
|
+
- `via` references the property on the join table that points back to this entity
|
|
118
|
+
- The join table must have `model` relationships to both sides
|
|
88
119
|
|
|
89
120
|
## Self-referencing relationships
|
|
90
121
|
|
|
91
|
-
|
|
92
|
-
from the table name, or set via `modelName` option):
|
|
122
|
+
Entities can reference themselves for hierarchical data:
|
|
93
123
|
|
|
94
124
|
```ts
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
})
|
|
101
|
-
|
|
125
|
+
@table({ name: 'categories' })
|
|
126
|
+
export class Category extends Entity {
|
|
127
|
+
@primaryColumn({ type: 'integer' })
|
|
128
|
+
public id!: number;
|
|
129
|
+
|
|
130
|
+
@column({ type: 'string', required: true })
|
|
131
|
+
public name!: string;
|
|
102
132
|
|
|
103
|
-
|
|
133
|
+
@column({ model: () => 'Category', name: 'parent_id' })
|
|
134
|
+
public parent?: number | Category | null;
|
|
135
|
+
|
|
136
|
+
@column({ collection: () => 'Category', via: 'parent' })
|
|
137
|
+
public children?: Category[];
|
|
138
|
+
}
|
|
139
|
+
```
|
|
104
140
|
|
|
105
141
|
## QueryResult type narrowing
|
|
106
142
|
|
|
107
|
-
When you query entities, BigAl returns `QueryResult<T>` which narrows relationship fields
|
|
108
|
-
automatically:
|
|
143
|
+
When you query entities, BigAl returns `QueryResult<T>` which automatically narrows relationship fields:
|
|
109
144
|
|
|
110
145
|
```ts
|
|
111
|
-
const product = await
|
|
146
|
+
const product = await productRepository.findOne().where({ id: 1 });
|
|
112
147
|
|
|
113
|
-
// product.store is `number`, not
|
|
148
|
+
// product.store is `number`, not `number | Store`
|
|
149
|
+
// QueryResult narrows the union automatically
|
|
114
150
|
console.log(product.store); // number (the foreign key ID)
|
|
115
151
|
```
|
|
116
152
|
|
|
117
153
|
The narrowing rules:
|
|
118
154
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
| `number
|
|
122
|
-
| `
|
|
155
|
+
| Entity property type | QueryResult type |
|
|
156
|
+
| ------------------------- | -------------------- |
|
|
157
|
+
| `number \| Store` | `number` |
|
|
158
|
+
| `number \| Store \| null` | `number \| null` |
|
|
159
|
+
| `Product[]` (collection) | Excluded from result |
|
|
123
160
|
|
|
124
161
|
### Using QueryResult in type definitions
|
|
125
162
|
|
|
126
|
-
`QueryResult
|
|
163
|
+
Use `Pick<QueryResult<T>, ...>` instead of `Pick<T, ...>` for derived types:
|
|
127
164
|
|
|
128
165
|
```ts
|
|
129
166
|
import type { QueryResult } from 'bigal';
|
|
130
167
|
|
|
131
|
-
// store is `number
|
|
132
|
-
type
|
|
168
|
+
// Correct: store is `number`
|
|
169
|
+
type ProductSummary = Pick<QueryResult<Product>, 'id' | 'name' | 'store'>;
|
|
133
170
|
|
|
134
|
-
//
|
|
135
|
-
type
|
|
171
|
+
// Wrong: store is `number | Store`
|
|
172
|
+
type ProductSummaryWrong = Pick<Product, 'id' | 'name' | 'store'>;
|
|
136
173
|
```
|
|
137
174
|
|
|
138
|
-
##
|
|
175
|
+
## QueryResultPopulated
|
|
139
176
|
|
|
140
|
-
|
|
141
|
-
typed. The full models map is threaded through repositories, so `.populate('store')` resolves to the
|
|
142
|
-
related entity type rather than `Record<string, unknown>`.
|
|
177
|
+
For type safety with populated relations:
|
|
143
178
|
|
|
144
179
|
```ts
|
|
145
|
-
|
|
180
|
+
import type { QueryResultPopulated } from 'bigal';
|
|
146
181
|
|
|
147
|
-
//
|
|
182
|
+
// store is QueryResult<Store>
|
|
183
|
+
type ProductWithStore = QueryResultPopulated<Product, 'store'>;
|
|
148
184
|
```
|
|
149
185
|
|
|
150
|
-
Array-style `initialize({ models: [...] })` still works but does not provide typed populate results.
|
|
151
|
-
|
|
152
186
|
## Populate with junction table filtering
|
|
153
187
|
|
|
154
188
|
For many-to-many relationships, you can filter and sort by columns on the junction table:
|
|
@@ -173,7 +207,8 @@ const compilation = await compilationRepository
|
|
|
173
207
|
|
|
174
208
|
## Best practices
|
|
175
209
|
|
|
176
|
-
1. **Use `QueryResult<
|
|
177
|
-
2. **Use string references for model
|
|
178
|
-
3. **
|
|
179
|
-
4. **
|
|
210
|
+
1. **Use `QueryResult<T>` for return types** — avoids union type ambiguity
|
|
211
|
+
2. **Use string references for model names** — prevents circular imports
|
|
212
|
+
3. **Mark collections as optional** — they are `undefined` unless populated
|
|
213
|
+
4. **Avoid type assertions** — `QueryResult` narrows types automatically
|
|
214
|
+
5. **Use `.toJSON()` for serializable results** — strips class prototypes
|