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
|
@@ -1,594 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Step-by-step guide for upgrading from BigAl v15 (decorators) to v16 (function-based schema API).
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Migrating to v16
|
|
6
|
-
|
|
7
|
-
BigAl v16 replaces the decorator-based model system with a function-based schema API. Models are now
|
|
8
|
-
plain objects defined with `table()` and PostgreSQL-native column builders. Types are inferred from the
|
|
9
|
-
schema definition - no separate interfaces, no `extends Entity`, no `experimentalDecorators`.
|
|
10
|
-
|
|
11
|
-
## Requirements
|
|
12
|
-
|
|
13
|
-
- **Node.js 22** or later (v15 supported Node.js 20.11.0+)
|
|
14
|
-
- No `experimentalDecorators` or `useDefineForClassFields` in your `tsconfig.json`
|
|
15
|
-
- Works with tsc, esbuild, SWC, Vite, and Playwright without transpiler flags
|
|
16
|
-
|
|
17
|
-
## Breaking changes at a glance
|
|
18
|
-
|
|
19
|
-
| v15 (decorators) | v16 (function-based) |
|
|
20
|
-
| ------------------------------------- | -------------------------------------------- |
|
|
21
|
-
| `class Product extends Entity` | `const Product = table('products', { ... })` |
|
|
22
|
-
| `@table()`, `@column()` decorators | `table()`, `text()`, `integer()`, etc. |
|
|
23
|
-
| `@primaryColumn({ type: 'integer' })` | `serial().primaryKey()` |
|
|
24
|
-
| `@column({ model: () => 'Store' })` | `belongsTo('Store')` |
|
|
25
|
-
| `@column({ collection: ... })` | `hasMany('Product').via('store')` |
|
|
26
|
-
| `initialize({ models, pool })` | `initialize({ pool, models: { ... } })` |
|
|
27
|
-
| `repos.Product as Repository<T>` | Typed destructuring or `getRepository()` |
|
|
28
|
-
| `Entity`, `EntityStatic` | `InferSelect<T>`, `InferInsert<T>` |
|
|
29
|
-
| `NotEntity<T>` | Removed (no longer needed) |
|
|
30
|
-
| `.toJSON()` | Removed (results are always plain objects) |
|
|
31
|
-
| `DEBUG_BIGAL` env var | `onQuery` callback (env var still works) |
|
|
32
|
-
| Lifecycle hooks as static methods | `hooks` option in `table()` |
|
|
33
|
-
|
|
34
|
-
## Model definition
|
|
35
|
-
|
|
36
|
-
v15 used decorated classes extending `Entity`:
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
// v15
|
|
40
|
-
import { column, createDateColumn, Entity, primaryColumn, table, updateDateColumn } from 'bigal';
|
|
41
|
-
|
|
42
|
-
@table({ name: 'products' })
|
|
43
|
-
export class Product extends Entity {
|
|
44
|
-
@primaryColumn({ type: 'integer' })
|
|
45
|
-
public id!: number;
|
|
46
|
-
|
|
47
|
-
@column({ type: 'string', required: true })
|
|
48
|
-
public name!: string;
|
|
49
|
-
|
|
50
|
-
@column({ type: 'string' })
|
|
51
|
-
public sku?: string;
|
|
52
|
-
|
|
53
|
-
@column({ type: 'integer', required: true, name: 'price_cents' })
|
|
54
|
-
public priceCents!: number;
|
|
55
|
-
|
|
56
|
-
@column({ type: 'boolean', required: true, defaultsTo: true, name: 'is_active' })
|
|
57
|
-
public isActive!: boolean;
|
|
58
|
-
|
|
59
|
-
@column({ type: 'json' })
|
|
60
|
-
public metadata?: { color?: string };
|
|
61
|
-
|
|
62
|
-
@createDateColumn()
|
|
63
|
-
public createdAt!: Date;
|
|
64
|
-
|
|
65
|
-
@updateDateColumn()
|
|
66
|
-
public updatedAt!: Date;
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
v16 uses function-based schema definitions. Column names are auto-derived from property keys
|
|
71
|
-
(snakeCase), so you no longer pass column names as arguments:
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
// v16
|
|
75
|
-
import { boolean, createdAt, defineTable as table, integer, jsonb, serial, text, updatedAt, varchar } from 'bigal';
|
|
76
|
-
|
|
77
|
-
export const Product = table('products', {
|
|
78
|
-
id: serial().primaryKey(),
|
|
79
|
-
name: text().notNull(),
|
|
80
|
-
sku: varchar({ length: 100 }),
|
|
81
|
-
priceCents: integer().notNull(),
|
|
82
|
-
isActive: boolean().notNull().default(true),
|
|
83
|
-
metadata: jsonb<{ color?: string }>(),
|
|
84
|
-
createdAt: createdAt(),
|
|
85
|
-
updatedAt: updatedAt(),
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Types are inferred -- no separate interface needed
|
|
89
|
-
import type { InferSelect, InferInsert } from 'bigal';
|
|
90
|
-
|
|
91
|
-
type ProductRow = InferSelect<(typeof Product)['schema']>;
|
|
92
|
-
type ProductInsert = InferInsert<(typeof Product)['schema']>;
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Key differences:
|
|
96
|
-
|
|
97
|
-
- No class, no `extends Entity`
|
|
98
|
-
- Column types are PostgreSQL-native (`text`, `integer`, `boolean`) instead of abstract (`'string'`, `'integer'`)
|
|
99
|
-
- Column names auto-derive from property keys - no explicit column name argument needed
|
|
100
|
-
- Nullability and defaults are expressed through chain methods (`.notNull()`, `.default()`)
|
|
101
|
-
- Types are inferred from the schema; no manual interface required
|
|
102
|
-
- Use `InferSelect` / `InferInsert` utility types (no `$inferSelect` / `$inferInsert` phantom types)
|
|
103
|
-
|
|
104
|
-
## Base class and shared columns
|
|
105
|
-
|
|
106
|
-
v15 used abstract class inheritance:
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
// v15
|
|
110
|
-
@table({ name: '' })
|
|
111
|
-
export abstract class BaseModel extends Entity {
|
|
112
|
-
@primaryColumn({ type: 'integer' })
|
|
113
|
-
public id!: number;
|
|
114
|
-
@createDateColumn()
|
|
115
|
-
public createdAt!: Date;
|
|
116
|
-
@updateDateColumn()
|
|
117
|
-
public updatedAt!: Date;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
@table({ name: 'products' })
|
|
121
|
-
export class Product extends BaseModel {
|
|
122
|
-
@column({ type: 'string', required: true })
|
|
123
|
-
public name!: string;
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
v16 uses plain objects with spread:
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
// v16 -- shared.ts
|
|
131
|
-
import { createdAt, serial, updatedAt } from 'bigal';
|
|
132
|
-
|
|
133
|
-
export const modelBase = {
|
|
134
|
-
id: serial().primaryKey(),
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export const timestamps = {
|
|
138
|
-
createdAt: createdAt(),
|
|
139
|
-
updatedAt: updatedAt(),
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// v16 -- Product.ts
|
|
143
|
-
import { defineTable as table, text } from 'bigal';
|
|
144
|
-
import { modelBase, timestamps } from './shared';
|
|
145
|
-
|
|
146
|
-
export const Product = table('products', {
|
|
147
|
-
...modelBase,
|
|
148
|
-
...timestamps,
|
|
149
|
-
name: text().notNull(),
|
|
150
|
-
});
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Shared columns are plain objects that you spread into each schema definition. No abstract classes or
|
|
154
|
-
inheritance chains.
|
|
155
|
-
|
|
156
|
-
## Column types
|
|
157
|
-
|
|
158
|
-
Each v15 column type maps to a PostgreSQL-native builder in v16. Builders take no arguments - column
|
|
159
|
-
names auto-derive from property keys:
|
|
160
|
-
|
|
161
|
-
| v15 decorator | v16 builder |
|
|
162
|
-
| ------------------------------------- | ------------------------------------------ |
|
|
163
|
-
| `@column({ type: 'string' })` | `text()` or `text<'a' \| 'b'>()` for enums |
|
|
164
|
-
| `@column({ type: 'integer' })` | `integer()` |
|
|
165
|
-
| `@column({ type: 'float' })` | `float()` or `double()` |
|
|
166
|
-
| `@column({ type: 'boolean' })` | `boolean()` |
|
|
167
|
-
| `@column({ type: 'date' })` | `date()` |
|
|
168
|
-
| `@column({ type: 'datetime' })` | `timestamptz()` |
|
|
169
|
-
| `@column({ type: 'json' })` | `json()` or `jsonb()` |
|
|
170
|
-
| `@column({ type: 'string[]' })` | `textArray()` |
|
|
171
|
-
| `@column({ type: 'integer[]' })` | `integerArray()` |
|
|
172
|
-
| `@column({ type: 'boolean[]' })` | `booleanArray()` |
|
|
173
|
-
| `@primaryColumn({ type: 'integer' })` | `serial().primaryKey()` |
|
|
174
|
-
|
|
175
|
-
Additional v16 builders with no v15 equivalent: `bigserial`, `bigint`, `smallint`, `uuid`, `bytea`,
|
|
176
|
-
`timestamp` (without timezone), `vector({ dimensions })`.
|
|
177
|
-
|
|
178
|
-
### Column modifiers
|
|
179
|
-
|
|
180
|
-
| v15 option | v16 chain method |
|
|
181
|
-
| --------------------- | ------------------------- |
|
|
182
|
-
| `required: true` | `.notNull()` |
|
|
183
|
-
| `defaultsTo: value` | `.default(value)` |
|
|
184
|
-
| Primary key | `.primaryKey()` |
|
|
185
|
-
| (no equivalent) | `.unique()` |
|
|
186
|
-
| `@createDateColumn()` | `createdAt()` convenience |
|
|
187
|
-
| `@updateDateColumn()` | `updatedAt()` convenience |
|
|
188
|
-
| `@versionColumn()` | `integer().version()` |
|
|
189
|
-
|
|
190
|
-
`serial()` and `bigserial()` imply `.notNull()` and `.default()` automatically - they always produce
|
|
191
|
-
a non-null number on select and are optional on insert.
|
|
192
|
-
|
|
193
|
-
`.primaryKey()` implies `.notNull()` and makes the column optional on insert (database assigns the
|
|
194
|
-
value).
|
|
195
|
-
|
|
196
|
-
## Relationships
|
|
197
|
-
|
|
198
|
-
v16 uses string model references. The FK column name is auto-derived from the property key.
|
|
199
|
-
|
|
200
|
-
### Many-to-one (belongsTo)
|
|
201
|
-
|
|
202
|
-
```ts
|
|
203
|
-
// v15
|
|
204
|
-
@column({ model: () => 'Store', name: 'store_id' })
|
|
205
|
-
public store!: number | Store;
|
|
206
|
-
|
|
207
|
-
// v16 -- FK column auto-derived as store_id
|
|
208
|
-
store: belongsTo('Store'),
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
The inferred select type for a `belongsTo` column is the FK type (typically `number`), not a union.
|
|
212
|
-
After `.populate('store')`, the type changes to the related entity.
|
|
213
|
-
|
|
214
|
-
### One-to-many (hasMany)
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
// v15
|
|
218
|
-
@column({ collection: () => 'Product', via: 'store' })
|
|
219
|
-
public products?: Product[];
|
|
220
|
-
|
|
221
|
-
// v16
|
|
222
|
-
products: hasMany('Product').via('store'),
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
`hasMany` columns appear in `InferSelect` as optional `Record<string, unknown>[]` to support
|
|
226
|
-
populate. `QueryResult` strips them from results, so they only appear after `.populate()`.
|
|
227
|
-
They are excluded from the insert type entirely.
|
|
228
|
-
|
|
229
|
-
### Many-to-many (hasMany with through)
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
// v15
|
|
233
|
-
@column({
|
|
234
|
-
collection: () => 'Category',
|
|
235
|
-
through: () => 'ProductCategory',
|
|
236
|
-
via: 'product',
|
|
237
|
-
})
|
|
238
|
-
public categories?: Category[];
|
|
239
|
-
|
|
240
|
-
// v16
|
|
241
|
-
categories: hasMany('Category')
|
|
242
|
-
.through('ProductCategory')
|
|
243
|
-
.via('product'),
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Circular references
|
|
247
|
-
|
|
248
|
-
String references avoid the circular import problem entirely. `belongsTo` and `hasMany` accept only
|
|
249
|
-
string model names - arrow functions are not supported:
|
|
250
|
-
|
|
251
|
-
```ts
|
|
252
|
-
// String references - no circular import needed
|
|
253
|
-
store: belongsTo('Store'),
|
|
254
|
-
products: hasMany('Product').via('store'),
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
Model names are resolved at `initialize()` time. All referenced models must be included in the
|
|
258
|
-
`models` object or array.
|
|
259
|
-
|
|
260
|
-
## Initialization
|
|
261
|
-
|
|
262
|
-
v16 uses `initialize()` with two calling styles:
|
|
263
|
-
|
|
264
|
-
```ts
|
|
265
|
-
// v15
|
|
266
|
-
import { initialize } from 'bigal';
|
|
267
|
-
import type { IRepository } from 'bigal';
|
|
268
|
-
|
|
269
|
-
const repos = initialize({ models: [Product, Store], pool });
|
|
270
|
-
const Product = repos.Product as IRepository<Product>;
|
|
271
|
-
|
|
272
|
-
// v16 -- object style (typed destructuring, recommended)
|
|
273
|
-
import { initialize } from 'bigal';
|
|
274
|
-
|
|
275
|
-
const { Product, Store } = initialize({
|
|
276
|
-
pool,
|
|
277
|
-
models: { Product, Store, Category, ProductCategory },
|
|
278
|
-
});
|
|
279
|
-
// Product is fully typed -- no assertion needed
|
|
280
|
-
|
|
281
|
-
// v16 -- array style (use getRepository)
|
|
282
|
-
const bigal = initialize({ pool, models: [Product, Store, Category, ProductCategory] });
|
|
283
|
-
const Product = bigal.getRepository(Product);
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
`initialize()` validates all relationship references at construction time. If a `belongsTo` or
|
|
287
|
-
`hasMany` points to a model not in the `models` object/array, it throws immediately.
|
|
288
|
-
|
|
289
|
-
### Repository type aliases
|
|
290
|
-
|
|
291
|
-
Use `Repository<typeof Product>` and `ReadonlyRepository<typeof Product>` for type annotations:
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
import type { Repository, ReadonlyRepository } from 'bigal';
|
|
295
|
-
|
|
296
|
-
function processProducts(repo: Repository<typeof Product>) {
|
|
297
|
-
return repo.find().where({ name: { contains: 'widget' } });
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### initialize options
|
|
302
|
-
|
|
303
|
-
| Option | Type | Description |
|
|
304
|
-
| -------------- | ------------------------------------------ | --------------------------------------------- |
|
|
305
|
-
| `pool` | `PoolLike` | Primary connection pool |
|
|
306
|
-
| `readonlyPool` | `PoolLike` | Pool for read operations (defaults to `pool`) |
|
|
307
|
-
| `models` | `Record<string, TableDefinition>` or array | All table definitions |
|
|
308
|
-
| `connections` | `Record<string, IConnection>` | Named connections for multi-database setups |
|
|
309
|
-
| `onQuery` | `OnQueryCallback` | Query observability callback |
|
|
310
|
-
|
|
311
|
-
## Results are always plain objects
|
|
312
|
-
|
|
313
|
-
In v15, query results were class instances with prototypes. You could call `.toJSON()` to get plain
|
|
314
|
-
objects.
|
|
315
|
-
|
|
316
|
-
In v16, all results are plain objects. There is no `.toJSON()` method and no class wrapper.
|
|
317
|
-
|
|
318
|
-
```ts
|
|
319
|
-
// v15
|
|
320
|
-
const product = await Product.findOne().where({ id: 42 }).toJSON();
|
|
321
|
-
|
|
322
|
-
// v16
|
|
323
|
-
const product = await Product.findOne().where({ id: 42 });
|
|
324
|
-
// Already a plain object -- no .toJSON() needed
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
## NotEntity\<T\> is removed
|
|
328
|
-
|
|
329
|
-
In v15, JSON columns with an `id` property required `NotEntity<T>` to prevent BigAl's type system from
|
|
330
|
-
treating them as entity relationships:
|
|
331
|
-
|
|
332
|
-
```ts
|
|
333
|
-
// v15
|
|
334
|
-
import type { NotEntity } from 'bigal';
|
|
335
|
-
interface IPayload { id: string; message: string; }
|
|
336
|
-
|
|
337
|
-
@column({ type: 'json' })
|
|
338
|
-
public payload?: NotEntity<IPayload>;
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
In v16, this is not needed. The type system uses column builder metadata (not structural typing on `id`)
|
|
342
|
-
to distinguish relationships from data columns:
|
|
343
|
-
|
|
344
|
-
```ts
|
|
345
|
-
// v16
|
|
346
|
-
interface IPayload { id: string; message: string; }
|
|
347
|
-
|
|
348
|
-
// In the schema definition:
|
|
349
|
-
payload: jsonb<IPayload>(),
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
## Type imports
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
// v15
|
|
356
|
-
import { Entity } from 'bigal';
|
|
357
|
-
import type { EntityStatic, QueryResult } from 'bigal';
|
|
358
|
-
|
|
359
|
-
// v16
|
|
360
|
-
import type { QueryResult, InferInsert, Repository } from 'bigal';
|
|
361
|
-
|
|
362
|
-
type ProductRow = QueryResult<typeof Product>;
|
|
363
|
-
|
|
364
|
-
// For repository type annotations:
|
|
365
|
-
function getProducts(repo: Repository<typeof Product>) {
|
|
366
|
-
/* ... */
|
|
367
|
-
}
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
`Entity` and `EntityStatic` are no longer needed. Use `QueryResult<typeof Model>` to get the row type
|
|
371
|
-
(excludes hasMany, narrows FKs), and `InferInsert` for the insert type. Use `Repository<typeof Model>` and
|
|
372
|
-
`ReadonlyRepository<typeof Model>` for repository type annotations.
|
|
373
|
-
|
|
374
|
-
## Lifecycle hooks
|
|
375
|
-
|
|
376
|
-
v15 used static methods on the class:
|
|
377
|
-
|
|
378
|
-
```ts
|
|
379
|
-
// v15
|
|
380
|
-
@table({ name: 'products' })
|
|
381
|
-
export class Product extends Entity {
|
|
382
|
-
// ... columns ...
|
|
383
|
-
public static beforeCreate(values: Partial<Product>): Partial<Product> {
|
|
384
|
-
return { ...values, slug: slugify(values.name!) };
|
|
385
|
-
}
|
|
386
|
-
public static beforeUpdate(values: Partial<Product>): Partial<Product> {
|
|
387
|
-
return { ...values, updatedBy: getCurrentUserId() };
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
v16 uses the `hooks` option in `table()` with all 7 lifecycle hooks:
|
|
393
|
-
|
|
394
|
-
```ts
|
|
395
|
-
// v16
|
|
396
|
-
export const Product = table(
|
|
397
|
-
'products',
|
|
398
|
-
{
|
|
399
|
-
// ... columns ...
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
hooks: {
|
|
403
|
-
beforeCreate(values) {
|
|
404
|
-
return { ...values, slug: slugify(values.name) };
|
|
405
|
-
},
|
|
406
|
-
afterCreate(result) {
|
|
407
|
-
audit.log('product.created', result.id);
|
|
408
|
-
},
|
|
409
|
-
beforeUpdate(values) {
|
|
410
|
-
return { ...values, updatedBy: getCurrentUserId() };
|
|
411
|
-
},
|
|
412
|
-
afterUpdate(result) {
|
|
413
|
-
audit.log('product.updated', result.id);
|
|
414
|
-
},
|
|
415
|
-
beforeDestroy(where) {
|
|
416
|
-
return where;
|
|
417
|
-
},
|
|
418
|
-
afterDestroy({ rowCount }) {
|
|
419
|
-
audit.log('product.destroyed', rowCount);
|
|
420
|
-
},
|
|
421
|
-
afterFind(results) {
|
|
422
|
-
return results;
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
);
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
The `values` parameter is fully typed based on the schema definition.
|
|
430
|
-
|
|
431
|
-
## New features in v16
|
|
432
|
-
|
|
433
|
-
### Global filters
|
|
434
|
-
|
|
435
|
-
Automatically apply WHERE clauses to every `find` and `findOne` query:
|
|
436
|
-
|
|
437
|
-
```ts
|
|
438
|
-
const Product = table(
|
|
439
|
-
'products',
|
|
440
|
-
{
|
|
441
|
-
/* columns */
|
|
442
|
-
},
|
|
443
|
-
{
|
|
444
|
-
filters: {
|
|
445
|
-
active: { isActive: true },
|
|
446
|
-
notDeleted: () => ({ deletedAt: null }),
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
);
|
|
450
|
-
|
|
451
|
-
// Override per query
|
|
452
|
-
await Product.find().where({}).filters(false);
|
|
453
|
-
await Product.find().where({}).filters({ active: false });
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
### toSQL()
|
|
457
|
-
|
|
458
|
-
Inspect the generated SQL without executing on any query operation:
|
|
459
|
-
|
|
460
|
-
```ts
|
|
461
|
-
const { sql, params } = Product.find().where({ name: 'Widget' }).toSQL();
|
|
462
|
-
// sql: 'SELECT ... FROM "products" WHERE "name"=$1'
|
|
463
|
-
// params: ['Widget']
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
Available on `find`, `findOne`, `create`, `update`, and `destroy`.
|
|
467
|
-
|
|
468
|
-
### pgvector support
|
|
469
|
-
|
|
470
|
-
Store and query vector embeddings with `vector({ dimensions })`:
|
|
471
|
-
|
|
472
|
-
```ts
|
|
473
|
-
import { vector } from 'bigal';
|
|
474
|
-
|
|
475
|
-
embedding: vector({ dimensions: 1536 }),
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
Query with nearest-neighbor sort:
|
|
479
|
-
|
|
480
|
-
```ts
|
|
481
|
-
await repo
|
|
482
|
-
.find()
|
|
483
|
-
.where({})
|
|
484
|
-
.sort({ embedding: { nearestTo: queryVector, metric: 'cosine' } });
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
### view()
|
|
488
|
-
|
|
489
|
-
Define read-only models backed by PostgreSQL views:
|
|
490
|
-
|
|
491
|
-
```ts
|
|
492
|
-
import { view } from 'bigal';
|
|
493
|
-
|
|
494
|
-
export const ProductSummary = view('product_summaries', {
|
|
495
|
-
/* columns */
|
|
496
|
-
});
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
### String references
|
|
500
|
-
|
|
501
|
-
Relationships use string model names. Arrow functions are not supported:
|
|
502
|
-
|
|
503
|
-
```ts
|
|
504
|
-
store: belongsTo('Store'),
|
|
505
|
-
products: hasMany('Product').via('store'),
|
|
506
|
-
categories: hasMany('Category').through('ProductCategory').via('product'),
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
Model names are auto-derived from table names (`products` becomes `Product`).
|
|
510
|
-
|
|
511
|
-
## Logging and observability
|
|
512
|
-
|
|
513
|
-
v15 used the `DEBUG_BIGAL` environment variable:
|
|
514
|
-
|
|
515
|
-
```sh
|
|
516
|
-
# v15
|
|
517
|
-
DEBUG_BIGAL=true node app.js
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
v16 adds a structured `onQuery` callback:
|
|
521
|
-
|
|
522
|
-
```ts
|
|
523
|
-
// v16
|
|
524
|
-
const { Product } = initialize({
|
|
525
|
-
pool,
|
|
526
|
-
models: { Product, Store },
|
|
527
|
-
onQuery({ sql, params, duration, error, model, operation }) {
|
|
528
|
-
logger.debug({ sql, params, duration, model, operation });
|
|
529
|
-
},
|
|
530
|
-
});
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
The `onQuery` callback receives structured data for every query. The `DEBUG_BIGAL` environment variable
|
|
534
|
-
still works as a fallback.
|
|
535
|
-
|
|
536
|
-
**Note:** `params` may contain sensitive data (user input, passwords, etc.). Use appropriate care when
|
|
537
|
-
logging.
|
|
538
|
-
|
|
539
|
-
## Removed exports
|
|
540
|
-
|
|
541
|
-
The following exports no longer exist in v16:
|
|
542
|
-
|
|
543
|
-
- `Entity`, `EntityStatic` - no base class needed
|
|
544
|
-
- `NotEntity`, `NotEntityBrand` - structural typing workaround no longer needed
|
|
545
|
-
- `@table()`, `@column()`, `@primaryColumn()`, `@createDateColumn()`, `@updateDateColumn()`,
|
|
546
|
-
`@versionColumn()` decorators
|
|
547
|
-
- `.toJSON()` on query results - results are already plain objects
|
|
548
|
-
- `FindResultJSON`, `FindOneResultJSON`, `CreateResultJSON`, `UpdateResultJSON`, `DestroyResultJSON`
|
|
549
|
-
|
|
550
|
-
## Query API is unchanged
|
|
551
|
-
|
|
552
|
-
The fluent query builder API is identical in v16. All of these work exactly as before:
|
|
553
|
-
|
|
554
|
-
```ts
|
|
555
|
-
// find with where, sort, limit
|
|
556
|
-
const products = await Product.find()
|
|
557
|
-
.where({ name: { contains: 'widget' } })
|
|
558
|
-
.sort('name')
|
|
559
|
-
.limit(10);
|
|
560
|
-
|
|
561
|
-
// findOne with populate
|
|
562
|
-
const product = await Product.findOne().where({ id: 42 }).populate('store');
|
|
563
|
-
|
|
564
|
-
// count
|
|
565
|
-
const count = await Product.count().where({ isActive: true });
|
|
566
|
-
|
|
567
|
-
// create
|
|
568
|
-
const newProduct = await Product.create({ name: 'Widget', priceCents: 999, store: 1 });
|
|
569
|
-
|
|
570
|
-
// update
|
|
571
|
-
const updated = await Product.update({ id: 42 }, { name: 'Super Widget' });
|
|
572
|
-
|
|
573
|
-
// destroy
|
|
574
|
-
await Product.destroy({ id: 42 });
|
|
575
|
-
|
|
576
|
-
// upsert
|
|
577
|
-
await Product.create({ name: 'Widget', sku: 'WDG-001', priceCents: 999 }, { onConflict: { action: 'merge', targets: ['sku'], merge: ['priceCents'] } });
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
## Migration checklist
|
|
581
|
-
|
|
582
|
-
1. Update Node.js to v22 or later
|
|
583
|
-
2. Remove `experimentalDecorators` and `useDefineForClassFields` from `tsconfig.json`
|
|
584
|
-
3. Convert each model class to a `table()` call with no-arg column builders
|
|
585
|
-
4. Replace `extends Entity` / abstract base classes with shared column objects using spread
|
|
586
|
-
5. Replace `@column({ model: ... })` with `belongsTo('ModelName')`
|
|
587
|
-
6. Replace `@column({ collection: ... })` with `hasMany('ModelName')`
|
|
588
|
-
7. Update `initialize()` call to use object-style models for typed destructuring
|
|
589
|
-
8. Remove `NotEntity<T>` wrappers from JSON column types
|
|
590
|
-
9. Remove `.toJSON()` calls from queries
|
|
591
|
-
10. Replace `Entity` / `EntityStatic` type imports with `InferSelect` / `InferInsert`
|
|
592
|
-
11. Replace `Repository<T>` type assertions with `Repository<typeof Model>`
|
|
593
|
-
12. Replace static lifecycle hooks with `hooks` option in `table()`
|
|
594
|
-
13. Replace `DEBUG_BIGAL` usage with `onQuery` callback (optional - env var still works)
|