bigal 16.0.0-beta.3 → 16.0.1

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,486 +1,187 @@
1
1
  ---
2
- description: Define models with the table() function and PostgreSQL-native column builders for type-safe schema definitions.
2
+ description: Define PostgreSQL tables as TypeScript classes with decorators for columns, primary keys, relationships, and automatic timestamps.
3
3
  ---
4
4
 
5
5
  # Models
6
6
 
7
- Models define the shape of your PostgreSQL tables. Each model is created with the `table()` function and
8
- a set of column builders that map directly to PostgreSQL column types. Types are inferred from the schema
9
- definition - no separate interfaces required.
7
+ Models map TypeScript classes to PostgreSQL tables. Every model extends `Entity` and uses decorators for table and column configuration.
10
8
 
11
- ## Defining a model
9
+ ## Table decorator
12
10
 
13
- Use `table()` to create a model:
11
+ Use `@table()` to bind a class to a database table:
14
12
 
15
13
  ```ts
16
- import { table, serial, text, integer, boolean, createdAt, updatedAt } from 'bigal';
17
-
18
- export const Product = table('products', {
19
- id: serial().primaryKey(),
20
- name: text().notNull(),
21
- priceCents: integer().notNull(),
22
- isActive: boolean().notNull().default(true),
23
- createdAt: createdAt(),
24
- updatedAt: updatedAt(),
25
- });
26
- ```
27
-
28
- The first argument is the database table name. The second is an object mapping property names to column
29
- builders. Column builders take no arguments by default - the database column name is auto-derived from
30
- the property key using snakeCase (e.g., `priceCents` becomes `price_cents`).
31
-
32
- ### Auto-derived names
33
-
34
- BigAl automatically derives two names from your definitions:
35
-
36
- - **Column names** - property keys are converted to snake_case for the database column name.
37
- `priceCents` becomes `price_cents`, `isActive` becomes `is_active`.
38
- - **Model names** - the table name is singularized and PascalCased for relationship lookups.
39
- `products` becomes `Product`, `product__category` becomes `ProductCategory`.
40
-
41
- Override either when the convention does not match:
14
+ import { table, Entity } from 'bigal';
42
15
 
43
- ```ts
44
- // Override column name
45
- aliases: textArray({ name: 'alias_names' }).default([]),
46
-
47
- // Override model name
48
- const Product = table('items', { /* ... */ }, { modelName: 'Product' });
16
+ @table({ name: 'products' })
17
+ export class Product extends Entity {
18
+ // columns go here
19
+ }
49
20
  ```
50
21
 
51
- ### Table options
52
-
53
- Pass a third argument for additional options:
54
-
55
- ```ts
56
- const AuditLog = table(
57
- 'audit_logs',
58
- {
59
- /* columns */
60
- },
61
- {
62
- schema: 'audit',
63
- readonly: true,
64
- connection: 'audit',
65
- },
66
- );
67
- ```
22
+ Options:
68
23
 
69
24
  | Option | Type | Description |
70
25
  | ------------ | --------- | -------------------------------------------------------- |
26
+ | `name` | `string` | Database table or view name |
71
27
  | `schema` | `string` | PostgreSQL schema (default: `public`) |
72
- | `readonly` | `boolean` | If `true`, `initialize()` returns a read-only repository |
28
+ | `readonly` | `boolean` | If `true`, `initialize()` returns a `ReadonlyRepository` |
73
29
  | `connection` | `string` | Named connection key (for multi-database setups) |
74
- | `modelName` | `string` | Override auto-derived model name |
75
- | `hooks` | `object` | Lifecycle hooks (see [Hooks](#hooks)) |
76
- | `filters` | `object` | Global filters (see [Global filters](#global-filters)) |
77
30
 
78
- ## Column types
31
+ ## Column decorators
79
32
 
80
- Each builder maps to a PostgreSQL column type. All columns are nullable by default - use `.notNull()`
81
- to make them required. Column builders take no arguments; pass `{ name: 'custom_name' }` only when the
82
- auto-derived snake_case name does not match your database column.
33
+ ### `@primaryColumn()`
83
34
 
84
- ### String types
35
+ Marks the primary key column:
85
36
 
86
37
  ```ts
87
- import { text, varchar, uuid } from 'bigal';
88
-
89
- name: text(), // TEXT - string | null
90
- sku: varchar({ length: 100 }), // VARCHAR(100) - string | null
91
- externalId: uuid(), // UUID - string | null
92
- status: text<'active' | 'inactive'>(), // TEXT - 'active' | 'inactive' | null
93
- role: varchar<'admin' | 'user'>({ length: 50 }).notNull(), // VARCHAR(50) - 'admin' | 'user'
94
- ```
38
+ import { primaryColumn } from 'bigal';
95
39
 
96
- `text()` and `varchar()` accept an optional generic to narrow the type to a string literal union.
97
- This is useful for columns that store enum-like values.
98
-
99
- ### Numeric types
100
-
101
- ```ts
102
- import { serial, bigserial, integer, bigint, smallint, float, double } from 'bigal';
103
-
104
- id: serial().primaryKey(), // SERIAL - number (notNull + default implied)
105
- bigId: bigserial(), // BIGSERIAL - number (notNull + default implied)
106
- quantity: integer(), // INTEGER - number | null
107
- views: bigint(), // BIGINT - number | null
108
- rank: smallint(), // SMALLINT - number | null
109
- score: float(), // REAL - number | null
110
- precise: double(), // DOUBLE PRECISION - number | null
40
+ @primaryColumn({ type: 'integer' })
41
+ public id!: number;
111
42
  ```
112
43
 
113
- `serial()` and `bigserial()` automatically imply `.notNull()` and `.default()`. They are always
114
- `number` on select and optional on insert.
44
+ ### `@column()`
115
45
 
116
- ### Boolean
46
+ Defines a regular column:
117
47
 
118
48
  ```ts
119
- import { boolean } from 'bigal';
120
-
121
- isActive: boolean(), // BOOLEAN -- boolean | null
122
- isPublished: boolean().notNull(), // BOOLEAN -- boolean
123
- isArchived: boolean().notNull().default(false), // BOOLEAN -- boolean (optional on insert)
124
- ```
125
-
126
- ### Date and time
127
-
128
- ```ts
129
- import { date, timestamp, timestamptz, createdAt, updatedAt } from 'bigal';
130
-
131
- birthDate: date(), // DATE -- Date | null
132
- occurredAt: timestamp(), // TIMESTAMP -- Date | null
133
- scheduledFor: timestamptz(), // TIMESTAMPTZ -- Date | null
134
- createdAt: createdAt(), // TIMESTAMPTZ -- Date (notNull, auto-set on insert)
135
- updatedAt: updatedAt(), // TIMESTAMPTZ -- Date (notNull, auto-set on insert/update)
136
- ```
137
-
138
- `createdAt()` and `updatedAt()` default to column names `created_at` and `updated_at`. Pass a custom
139
- name if needed: `createdAt({ name: 'creation_time' })`.
140
-
141
- ### JSON
49
+ import { column } from 'bigal';
142
50
 
143
- ```ts
144
- import { json, jsonb } from 'bigal';
51
+ @column({ type: 'string', required: true })
52
+ public name!: string;
145
53
 
146
- settings: json<{ theme: string }>(), // JSON -- { theme: string } | null
147
- metadata: jsonb<{ color?: string }>(), // JSONB -- { color?: string } | null
54
+ @column({ type: 'string' })
55
+ public sku?: string;
148
56
  ```
149
57
 
150
- The generic parameter controls the TypeScript type. If omitted, it defaults to
151
- `Record<string, unknown>`.
58
+ ### Vector (pgvector)
152
59
 
153
- ### Binary
60
+ Declare a `VECTOR(n)` column with `type: 'vector'`. Values are `number[] | null`:
154
61
 
155
62
  ```ts
156
- import { bytea } from 'bigal';
63
+ import { column } from 'bigal';
157
64
 
158
- data: bytea(), // BYTEA -- Buffer | null
65
+ @column({ type: 'vector', dimensions: 1536 })
66
+ public embedding?: number[];
159
67
  ```
160
68
 
161
- ### Array types
162
-
163
- ```ts
164
- import { textArray, integerArray, booleanArray } from 'bigal';
69
+ Requires the [pgvector](https://github.com/pgvector/pgvector) extension. The `dimensions` option is
70
+ informational — BigAl does not issue DDL. See
71
+ [Querying > Vector distance queries](/guide/querying#vector-distance-queries) for sorting and
72
+ filtering by similarity.
165
73
 
166
- tags: textArray(), // TEXT[] -- string[] | null
167
- scores: integerArray(), // INTEGER[] -- number[] | null
168
- flags: booleanArray(), // BOOLEAN[] -- boolean[] | null
169
- ```
74
+ ### `@createDateColumn()`
170
75
 
171
- ### Vector (pgvector)
76
+ Automatically set on insert:
172
77
 
173
78
  ```ts
174
- import { vector } from 'bigal';
175
-
176
- embedding: vector({ dimensions: 1536 }), // VECTOR(1536) -- number[] | null
177
- ```
178
-
179
- The `dimensions` option is required. See [Querying > Vector distance](/guide/querying#vector-distance-queries)
180
- for sorting and filtering by similarity.
79
+ import { createDateColumn } from 'bigal';
181
80
 
182
- ## Column modifiers
183
-
184
- Chain methods on any column builder to set constraints. Each method returns the builder, so chains
185
- compose naturally.
186
-
187
- ### .notNull()
188
-
189
- Removes `null` from the TypeScript type. Maps to a `NOT NULL` constraint.
190
-
191
- ```ts
192
- name: text().notNull(), // string (not string | null)
81
+ @createDateColumn()
82
+ public createdAt!: Date;
193
83
  ```
194
84
 
195
- ### .default(value)
85
+ ### `@updateDateColumn()`
196
86
 
197
- Makes the column optional on insert. The value is used as the TypeScript default hint.
87
+ Automatically set on update:
198
88
 
199
89
  ```ts
200
- isActive: boolean().notNull().default(true), // boolean, optional on insert
201
- aliases: textArray().default([]), // string[] | null, optional on insert
202
- ```
90
+ import { updateDateColumn } from 'bigal';
203
91
 
204
- ### .primaryKey()
205
-
206
- Marks the column as the primary key. Implies `.notNull()` and makes the column optional on insert
207
- (the database assigns the value).
208
-
209
- ```ts
210
- id: serial().primaryKey(), // number, optional on insert
211
- id: uuid().primaryKey(), // string, optional on insert
92
+ @updateDateColumn()
93
+ public updatedAt!: Date;
212
94
  ```
213
95
 
214
- ### .unique()
96
+ ### `@versionColumn()`
215
97
 
216
- Adds a UNIQUE constraint. No effect on the TypeScript type.
98
+ Auto-incrementing version for optimistic locking:
217
99
 
218
100
  ```ts
219
- email: text().notNull().unique(), // string, must be unique
220
- ```
221
-
222
- ### .version()
223
-
224
- Marks the column for optimistic locking. Implies `.notNull()`. BigAl automatically increments the
225
- value on each update.
101
+ import { versionColumn } from 'bigal';
226
102
 
227
- ```ts
228
- revision: integer().version(), // number, auto-incremented on update
103
+ @versionColumn()
104
+ public version!: number;
229
105
  ```
230
106
 
231
- ### Chaining
232
-
233
- Modifiers compose in any order:
234
-
235
- ```ts
236
- email: text().notNull().unique(),
237
- isActive: boolean().notNull().default(true),
238
- score: integer().default(0),
239
- ```
107
+ ## Column options
240
108
 
241
- ## Complete column reference
242
-
243
- | Builder | PostgreSQL type | TypeScript type | Notes |
244
- | ------------------------ | ---------------- | ------------------- | --------------------------------------- |
245
- | `serial()` | SERIAL | `number` | notNull + default implied |
246
- | `bigserial()` | BIGSERIAL | `number` | notNull + default implied |
247
- | `text<T>()` | TEXT | `T \| null` | Defaults to `string`; narrow with union |
248
- | `varchar<T>({ length })` | VARCHAR(n) | `T \| null` | Defaults to `string`; narrow with union |
249
- | `integer()` | INTEGER | `number \| null` | |
250
- | `bigint()` | BIGINT | `number \| null` | |
251
- | `smallint()` | SMALLINT | `number \| null` | |
252
- | `float()` / `real()` | REAL | `number \| null` | |
253
- | `double()` | DOUBLE PRECISION | `number \| null` | |
254
- | `boolean()` | BOOLEAN | `boolean \| null` | |
255
- | `timestamp()` | TIMESTAMP | `Date \| null` | |
256
- | `timestamptz()` | TIMESTAMPTZ | `Date \| null` | |
257
- | `date()` | DATE | `Date \| null` | |
258
- | `json<T>()` | JSON | `T \| null` | Defaults to `Record<string, unknown>` |
259
- | `jsonb<T>()` | JSONB | `T \| null` | Defaults to `Record<string, unknown>` |
260
- | `uuid()` | UUID | `string \| null` | |
261
- | `bytea()` | BYTEA | `Buffer \| null` | |
262
- | `textArray()` | TEXT[] | `string[] \| null` | |
263
- | `integerArray()` | INTEGER[] | `number[] \| null` | |
264
- | `booleanArray()` | BOOLEAN[] | `boolean[] \| null` | |
265
- | `vector({ dimensions })` | VECTOR(n) | `number[] \| null` | Requires pgvector extension |
266
- | `createdAt()` | TIMESTAMPTZ | `Date` | notNull, auto-set on insert |
267
- | `updatedAt()` | TIMESTAMPTZ | `Date` | notNull, auto-set on insert/update |
109
+ | Option | Type | Description |
110
+ | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
111
+ | `type` | `string` | Column type: `'string'`, `'integer'`, `'float'`, `'boolean'`, `'date'`, `'datetime'`, `'json'`, `'string[]'`, `'integer[]'`, `'float[]'`, `'boolean[]'`, `'vector'` |
112
+ | `name` | `string` | Database column name (if different from property name) |
113
+ | `required` | `boolean` | If `true`, value must not be null |
114
+ | `defaultsTo` | `any` | Default value |
115
+ | `dimensions` | `number` | Number of dimensions for `'vector'` columns. Informational only — BigAl does not issue DDL |
116
+ | `model` | `() => string` | Foreign key relationship (many-to-one) |
117
+ | `collection` | `() => string` | Inverse relationship (one-to-many or many-to-many) |
118
+ | `through` | `() => string` | Join table for many-to-many |
119
+ | `via` | `string` | Property on related model that holds the foreign key |
268
120
 
269
121
  ## Relationships
270
122
 
271
- ### Many-to-one (belongsTo)
272
-
273
- Use `belongsTo` when this table holds the foreign key:
123
+ ### Many-to-one
274
124
 
275
- ```ts
276
- import { belongsTo } from 'bigal';
277
-
278
- store: belongsTo('Store'),
279
- ```
280
-
281
- The argument is the model name of the related table. The FK column name is auto-derived as
282
- `snakeCase(propertyKey) + '_id'` (e.g., `store` becomes `store_id`).
283
-
284
- Override the FK column name when the convention does not match:
125
+ Use `model` when the current entity holds the foreign key:
285
126
 
286
127
  ```ts
287
- store: belongsTo('Store', { name: 'shop_id' }),
288
- ```
128
+ import { column, Entity, primaryColumn, table } from 'bigal';
129
+ import type { Store } from './Store';
289
130
 
290
- In the inferred select type, a `belongsTo` column is typed as the FK value (typically `number`). After
291
- `.populate('store')`, the type changes to the full related entity.
131
+ @table({ name: 'products' })
132
+ export class Product extends Entity {
133
+ @primaryColumn({ type: 'integer' })
134
+ public id!: number;
292
135
 
293
- ### One-to-many (hasMany)
294
-
295
- Use `hasMany` for the inverse side of a relationship:
296
-
297
- ```ts
298
- import { hasMany } from 'bigal';
299
-
300
- products: hasMany('Product').via('store'),
136
+ @column({ model: () => 'Store', name: 'store_id' })
137
+ public store!: number | Store;
138
+ }
301
139
  ```
302
140
 
303
- `.via()` specifies the property name on the related table that holds the foreign key back to this
304
- table.
141
+ The property type is `number | Store` it holds the foreign key when not populated, or the full entity after `.populate()`.
305
142
 
306
- ### Many-to-many (hasMany with through)
143
+ ### One-to-many
307
144
 
308
- Use `.through()` for relationships via a junction table:
145
+ Use `collection` on the inverse side:
309
146
 
310
147
  ```ts
311
- categories: hasMany('Category')
312
- .through('ProductCategory')
313
- .via('product'),
148
+ @column({ collection: () => 'Product', via: 'store' })
149
+ public products?: Product[];
314
150
  ```
315
151
 
316
- `hasMany` columns appear in `InferSelect` as optional `Record<string, unknown>[]` to support
317
- populate. `QueryResult` strips them from results, so they only appear after `.populate()`.
318
- They are excluded from the insert type entirely.
319
-
320
- See [Relationships](/guide/relationships) for complete examples.
152
+ Collections **must** be optional (`?`) since they are only present after `.populate()`.
321
153
 
322
- ## Shared columns
154
+ ### Many-to-many
323
155
 
324
- Extract a shared base only for columns that appear in every model (typically `id` + timestamps).
325
- Domain-specific columns like relationships should stay inline in each model.
156
+ Use `collection` with `through` for join tables:
326
157
 
327
158
  ```ts
328
- const modelBase = {
329
- id: serial().primaryKey(),
330
- createdAt: createdAt(),
331
- updatedAt: updatedAt(),
332
- };
333
-
334
- export const Product = table('products', {
335
- ...modelBase,
336
- name: text().notNull(),
337
- store: belongsTo('Store'),
338
- });
339
-
340
- export const Store = table('stores', {
341
- ...modelBase,
342
- name: text(),
343
- });
159
+ @column({
160
+ collection: () => 'Category',
161
+ through: () => 'ProductCategory',
162
+ via: 'product',
163
+ })
164
+ public categories?: Category[];
344
165
  ```
345
166
 
346
- ## Type inference
167
+ See [Relationships](/guide/relationships) for complete examples including join tables and self-referencing models.
347
168
 
348
- Use `QueryResult` to extract the row type from a model, and `InferInsert` for insert parameters:
169
+ ## Entity base class
349
170
 
350
- ```ts
351
- import type { QueryResult, InferInsert } from 'bigal';
352
-
353
- type ProductRow = QueryResult<typeof Product>;
354
- // { id: number; name: string; priceCents: number; isActive: boolean;
355
- // store: number; metadata: { color?: string } | null; createdAt: Date; updatedAt: Date }
356
- // hasMany collections (categories) are stripped by QueryResult
357
-
358
- type ProductInsert = InferInsert<(typeof Product)['schema']>;
359
- // { name: string; priceCents: number; store: number; <-- required
360
- // id?: number; isActive?: boolean; <-- optional (has default or primary key)
361
- // metadata?: { color?: string } | null; } <-- optional (nullable)
362
- ```
171
+ All models extend `Entity`, which provides no properties by itself — it serves as a marker for BigAl's type system to distinguish ORM entities from plain objects.
363
172
 
364
- ### Repository and ReadonlyRepository type aliases
173
+ ## NotEntity\<T\>
365
174
 
366
- When you need to annotate a function parameter or variable with a repository type, use the
367
- `Repository` and `ReadonlyRepository` type aliases:
175
+ If a JSON column contains objects with an `id` field, TypeScript may mistake them for BigAl entities. Wrap the type with `NotEntity<T>`:
368
176
 
369
177
  ```ts
370
- import type { Repository, ReadonlyRepository } from 'bigal';
371
-
372
- function processProducts(repo: Repository<typeof Product>) {
373
- return repo.find().where({ name: { contains: 'widget' } });
374
- }
178
+ import type { NotEntity } from 'bigal';
375
179
 
376
- function readSummary(repo: ReadonlyRepository<typeof StoreSummary>) {
377
- return repo.findOne().where({ id: 1 });
180
+ interface IMyJsonType {
181
+ id: string;
182
+ foo: string;
378
183
  }
379
- ```
380
-
381
- These accept `typeof YourTableDef` and resolve to the correctly typed `IRepository` or
382
- `IReadonlyRepository`.
383
-
384
- ### Insert type rules
385
-
386
- Columns are **required** on insert when they are `.notNull()` and have no default, are not a primary
387
- key, and are not auto-set.
388
-
389
- Columns are **optional** on insert when any of the following is true:
390
-
391
- - Nullable (no `.notNull()`)
392
- - Has a `.default()` value
393
- - Is a `.primaryKey()`
394
- - Is auto-set (`createdAt()`, `updatedAt()`)
395
-
396
- `hasMany` columns are excluded from the insert type entirely.
397
-
398
- ## Hooks
399
-
400
- Define lifecycle hooks in the third argument to `table()`:
401
-
402
- ```ts
403
- export const Product = table(
404
- 'products',
405
- {
406
- id: serial().primaryKey(),
407
- name: text().notNull(),
408
- slug: text().notNull(),
409
- },
410
- {
411
- hooks: {
412
- beforeCreate(values) {
413
- return { ...values, slug: slugify(values.name) };
414
- },
415
- afterCreate(result) {
416
- audit.log('product.created', result.id);
417
- },
418
- beforeUpdate(values) {
419
- if (values.name) {
420
- return { ...values, slug: slugify(values.name) };
421
- }
422
- return values;
423
- },
424
- afterUpdate(result) {
425
- audit.log('product.updated', result.id);
426
- },
427
- beforeDestroy(where) {
428
- return where;
429
- },
430
- afterDestroy({ rowCount }) {
431
- audit.log('product.destroyed', rowCount);
432
- },
433
- afterFind(results) {
434
- return results;
435
- },
436
- },
437
- },
438
- );
439
- ```
440
184
 
441
- | Hook | Called | Receives | Returns |
442
- | --------------- | --------------- | ---------------------- | ---------------------- |
443
- | `beforeCreate` | Before `INSERT` | Insert values | Modified insert values |
444
- | `afterCreate` | After `INSERT` | Created record | void |
445
- | `beforeUpdate` | Before `UPDATE` | Partial update values | Modified update values |
446
- | `afterUpdate` | After `UPDATE` | Updated record | void |
447
- | `beforeDestroy` | Before `DELETE` | Where clause object | Modified where clause |
448
- | `afterDestroy` | After `DELETE` | `{ rowCount: number }` | void |
449
- | `afterFind` | After `SELECT` | Array of results | Modified results array |
450
-
451
- All hook parameters are fully typed based on the schema definition. Hooks can be synchronous or
452
- return a Promise.
453
-
454
- ## Global filters
455
-
456
- Define named filters that are automatically applied to every `find` and `findOne` query:
457
-
458
- ```ts
459
- const Product = table(
460
- 'products',
461
- {
462
- /* columns */
463
- },
464
- {
465
- filters: {
466
- active: { isActive: true },
467
- notDeleted: () => ({ deletedAt: null }),
468
- },
469
- },
470
- );
471
- ```
472
-
473
- Filters can be static objects or functions that return a where clause dynamically. They are merged
474
- into every query's WHERE clause.
475
-
476
- Override filters per query:
477
-
478
- ```ts
479
- // Disable all filters
480
- await Product.find().where({}).filters(false);
481
-
482
- // Disable a specific filter
483
- await Product.find().where({}).filters({ active: false });
185
+ @column({ type: 'json' })
186
+ public metadata?: NotEntity<IMyJsonType>;
484
187
  ```
485
-
486
- See [Querying > Global filters](/guide/querying#global-filters) for more details.