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.
@@ -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)