bigal 16.0.0-beta.3 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,7 +68,6 @@ export default defineConfig({
68
68
  { text: 'Relationships', link: '/guide/relationships' },
69
69
  { text: 'Subqueries & Joins', link: '/guide/subqueries-and-joins' },
70
70
  { text: 'Views', link: '/guide/views' },
71
- { text: 'Migrating to v16', link: '/guide/migration-v16' },
72
71
  ],
73
72
  },
74
73
  {
@@ -29,35 +29,35 @@ Drop to raw SQL (via your pool directly) when:
29
29
 
30
30
  ### Basic queries
31
31
 
32
- | SQL | BigAl |
33
- | ----------------------------------------------------- | -------------------------------------------------------- |
34
- | `SELECT * FROM products WHERE id = 1` | `Product.findOne().where({ id: 1 })` |
35
- | `SELECT name FROM products WHERE id = 1` | `Product.findOne({ select: ['name'] }).where({ id: 1 })` |
36
- | `SELECT * FROM products WHERE name ILIKE '%widget%'` | `Product.find().where({ name: { contains: 'widget' } })` |
37
- | `SELECT * FROM products WHERE price >= 100` | `Product.find().where({ price: { '>=': 100 } })` |
38
- | `SELECT * FROM products WHERE status IN ('a','b')` | `Product.find().where({ status: ['a', 'b'] })` |
39
- | `SELECT * FROM products WHERE status <> 'x'` | `Product.find().where({ status: { '!': 'x' } })` |
40
- | `SELECT * FROM products WHERE deleted_at IS NOT NULL` | `Product.find().where({ deletedAt: { '!': null } })` |
41
- | `SELECT * FROM products ORDER BY name LIMIT 10` | `Product.find().where({}).sort('name asc').limit(10)` |
42
- | `SELECT COUNT(*) FROM products WHERE active = true` | `Product.count().where({ active: true })` |
32
+ | SQL | BigAl |
33
+ | ----------------------------------------------------- | ------------------------------------------------------------ |
34
+ | `SELECT * FROM products WHERE id = 1` | `productRepo.findOne().where({ id: 1 })` |
35
+ | `SELECT name FROM products WHERE id = 1` | `productRepo.findOne({ select: ['name'] }).where({ id: 1 })` |
36
+ | `SELECT * FROM products WHERE name ILIKE '%widget%'` | `productRepo.find().where({ name: { contains: 'widget' } })` |
37
+ | `SELECT * FROM products WHERE price >= 100` | `productRepo.find().where({ price: { '>=': 100 } })` |
38
+ | `SELECT * FROM products WHERE status IN ('a','b')` | `productRepo.find().where({ status: ['a', 'b'] })` |
39
+ | `SELECT * FROM products WHERE status <> 'x'` | `productRepo.find().where({ status: { '!': 'x' } })` |
40
+ | `SELECT * FROM products WHERE deleted_at IS NOT NULL` | `productRepo.find().where({ deletedAt: { '!': null } })` |
41
+ | `SELECT * FROM products ORDER BY name LIMIT 10` | `productRepo.find().where({}).sort('name asc').limit(10)` |
42
+ | `SELECT COUNT(*) FROM products WHERE active = true` | `productRepo.count().where({ active: true })` |
43
43
 
44
44
  ### CRUD
45
45
 
46
- | SQL | BigAl |
47
- | ----------------------------------------------------------- | ------------------------------------------ |
48
- | `INSERT INTO products (name) VALUES ('Widget') RETURNING *` | `Product.create({ name: 'Widget' })` |
49
- | `UPDATE products SET name = 'X' WHERE id = 1 RETURNING *` | `Product.update({ id: 1 }, { name: 'X' })` |
50
- | `DELETE FROM products WHERE id = 1 RETURNING *` | `Product.destroy({ id: 1 })` |
46
+ | SQL | BigAl |
47
+ | ----------------------------------------------------------- | ---------------------------------------------- |
48
+ | `INSERT INTO products (name) VALUES ('Widget') RETURNING *` | `productRepo.create({ name: 'Widget' })` |
49
+ | `UPDATE products SET name = 'X' WHERE id = 1 RETURNING *` | `productRepo.update({ id: 1 }, { name: 'X' })` |
50
+ | `DELETE FROM products WHERE id = 1 RETURNING *` | `productRepo.destroy({ id: 1 })` |
51
51
 
52
52
  ### Subqueries, joins, and advanced
53
53
 
54
- | SQL | BigAl |
55
- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
56
- | `WHERE store_id IN (SELECT id FROM stores WHERE active)` | `.where({ store: { in: subquery(Store).select(['id']).where({ active: true }) } })` |
57
- | `INNER JOIN stores s ON p.store_id = s.id WHERE s.name = 'Acme'` | `.join('store').where({ store: { name: 'Acme' } })` |
58
- | `SELECT DISTINCT ON (store_id) * ... ORDER BY store_id, created_at DESC` | `.distinctOn(['store']).sort('store').sort('createdAt desc')` |
59
- | `ON CONFLICT (sku) DO NOTHING` | `{ onConflict: { action: 'ignore', targets: ['sku'] } }` |
60
- | `ON CONFLICT (sku) DO UPDATE SET name = EXCLUDED.name` | `{ onConflict: { action: 'merge', targets: ['sku'], merge: ['name'] } }` |
54
+ | SQL | BigAl |
55
+ | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
56
+ | `WHERE store_id IN (SELECT id FROM stores WHERE active)` | `.where({ store: { in: subquery(storeRepo).select(['id']).where({ active: true }) } })` |
57
+ | `INNER JOIN stores s ON p.store_id = s.id WHERE s.name = 'Acme'` | `.join('store').where({ store: { name: 'Acme' } })` |
58
+ | `SELECT DISTINCT ON (store_id) * ... ORDER BY store_id, created_at DESC` | `.distinctOn(['store']).sort('store').sort('createdAt desc')` |
59
+ | `ON CONFLICT (sku) DO NOTHING` | `{ onConflict: { action: 'ignore', targets: ['sku'] } }` |
60
+ | `ON CONFLICT (sku) DO UPDATE SET name = EXCLUDED.name` | `{ onConflict: { action: 'merge', targets: ['sku'], merge: ['name'] } }` |
61
61
 
62
62
  ## Mixing BigAl and raw SQL
63
63
 
@@ -1,42 +1,47 @@
1
1
  ---
2
- description: Known issues and workarounds - hasMany collections with populate, and query observability.
2
+ description: Known issues and workarounds optional collections, NotEntity for JSON objects with id fields, and DEBUG_BIGAL logging.
3
3
  ---
4
4
 
5
5
  # Known Issues
6
6
 
7
- ## hasMany collections require populate
7
+ ## Entity collections must be optional
8
8
 
9
- `hasMany` relationships are excluded from the select and insert types. They are only present
10
- on query results after calling `.populate()`:
9
+ Collection properties (one-to-many, many-to-many) must be declared as optional. They are only present after `.populate()` and will cause `QueryResult` type errors if required:
11
10
 
12
11
  ```ts
13
- // categories is NOT on the result type
14
- const product = await Product.findOne().where({ id: 42 });
12
+ // Correct
13
+ @column({ collection: () => 'Product', via: 'store' })
14
+ public products?: Product[];
15
15
 
16
- // categories IS on the result type after populate
17
- const populated = await Product.findOne().where({ id: 42 }).populate('categories');
16
+ // Incorrect causes type issues
17
+ @column({ collection: () => 'Product', via: 'store' })
18
+ public products!: Product[];
18
19
  ```
19
20
 
20
- ## Query observability
21
+ ## Non-entity objects with id fields
21
22
 
22
- Use the `onQuery` callback in `initialize()` for structured query logging:
23
+ If a JSON column contains objects with an `id` property, TypeScript may mistake them for BigAl entities. Wrap the type with `NotEntity<T>`:
23
24
 
24
25
  ```ts
25
- const { Product } = initialize({
26
- pool,
27
- models: { Product },
28
- onQuery({ sql, params, duration, error, model, operation }) {
29
- logger.debug({ sql, params, duration, model, operation });
30
- },
31
- });
26
+ import type { NotEntity } from 'bigal';
27
+
28
+ interface IMyJsonType {
29
+ id: string;
30
+ foo: string;
31
+ }
32
+
33
+ @column({ type: 'json' })
34
+ public metadata?: NotEntity<IMyJsonType>;
32
35
  ```
33
36
 
34
- The `DEBUG_BIGAL` environment variable still works as a fallback when no `onQuery` callback is
35
- provided:
37
+ Without `NotEntity`, BigAl's type system treats the type as an entity relationship, which leads to incorrect type narrowing in `QueryResult`.
38
+
39
+ ## Debugging queries
40
+
41
+ Set the `DEBUG_BIGAL` environment variable to see generated SQL:
36
42
 
37
43
  ```sh
38
44
  DEBUG_BIGAL=true node app.js
39
45
  ```
40
46
 
41
- This logs all SQL statements and parameter values to the console. Note that `params` may contain
42
- sensitive data.
47
+ This logs all SQL statements and parameter values to the console.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Install BigAl, define your first model, create a repository, and run type-safe PostgreSQL queries.
2
+ description: Install BigAl, define your first model with decorators, initialize a repository, and run type-safe PostgreSQL queries.
3
3
  ---
4
4
 
5
5
  # Getting Started
@@ -25,56 +25,63 @@ npm install @neondatabase/serverless
25
25
 
26
26
  ## Define a model
27
27
 
28
- Models are defined with the `table()` function and PostgreSQL-native column builders. Types are
29
- inferred from the schema definition. Column names are auto-derived from property keys using snakeCase.
28
+ Models extend `Entity` and use decorators to map to database tables.
30
29
 
31
30
  ```ts
32
- import { createdAt, table, integer, serial, text, updatedAt, varchar } from 'bigal';
33
-
34
- export const Product = table('products', {
35
- id: serial().primaryKey(),
36
- name: text().notNull(),
37
- sku: varchar({ length: 100 }),
38
- priceCents: integer().notNull(),
39
- createdAt: createdAt(),
40
- updatedAt: updatedAt(),
41
- });
31
+ import { column, primaryColumn, table, Entity } from 'bigal';
32
+
33
+ @table({ name: 'products' })
34
+ export class Product extends Entity {
35
+ @primaryColumn({ type: 'integer' })
36
+ public id!: number;
37
+
38
+ @column({ type: 'string', required: true })
39
+ public name!: string;
40
+
41
+ @column({ type: 'string' })
42
+ public sku?: string;
43
+
44
+ @column({ type: 'integer', required: true, name: 'price_cents' })
45
+ public priceCents!: number;
46
+ }
42
47
  ```
43
48
 
44
49
  ## Initialize repositories
45
50
 
46
- Pass your models and a connection pool to `initialize()`. The object-style models option gives you
47
- typed repositories directly via destructuring:
51
+ Pass your models and a connection pool to `initialize()`. It returns a map of repositories keyed by model name.
48
52
 
49
53
  ```ts
50
- import { initialize } from 'bigal';
54
+ import { initialize, Repository } from 'bigal';
51
55
  import { Pool } from 'postgres-pool';
52
56
  import { Product } from './Product';
53
57
 
54
58
  const pool = new Pool('postgres://localhost/mydb');
55
59
 
56
- const { Product } = initialize({
57
- models: { Product },
60
+ const repos = initialize({
61
+ models: [Product],
58
62
  pool,
59
63
  });
64
+
65
+ const productRepository = repos.Product as Repository<Product>;
60
66
  ```
61
67
 
62
68
  ## Run your first query
63
69
 
64
- Queries use a fluent builder and are `PromiseLike` - just `await` the chain.
70
+ Queries use a fluent builder and are `PromiseLike` just `await` the chain.
65
71
 
66
72
  ```ts
67
73
  // Find all products with price >= 1000 cents, sorted by name
68
- const products = await Product.find()
74
+ const products = await productRepository
75
+ .find()
69
76
  .where({ priceCents: { '>=': 1000 } })
70
77
  .sort('name asc')
71
78
  .limit(10);
72
79
 
73
80
  // Find one product by ID
74
- const product = await Product.findOne().where({ id: 42 });
81
+ const product = await productRepository.findOne().where({ id: 42 });
75
82
 
76
83
  // Count matching records
77
- const count = await Product.count().where({ sku: { '!': null } });
84
+ const count = await productRepository.count().where({ sku: { '!': null } });
78
85
  ```
79
86
 
80
87
  ## Using with AI assistants
@@ -88,13 +95,12 @@ npx skills add bigalorm/bigal
88
95
 
89
96
  Machine-readable documentation is also available:
90
97
 
91
- - [llms.txt](/llms.txt) - structured overview
92
- - [llms-full.txt](/llms-full.txt) - complete documentation in a single file
98
+ - [llms.txt](/llms.txt) structured overview
99
+ - [llms-full.txt](/llms-full.txt) complete documentation in a single file
93
100
 
94
101
  ## Next steps
95
102
 
96
- - [Models](/guide/models) - table definitions, column types, relationships
97
- - [Querying](/guide/querying) - operators, pagination, JSONB, and more
98
- - [CRUD Operations](/guide/crud-operations) - create, update, and destroy
99
- - [API Reference](/reference/api) - all exports and method signatures
100
- - [Migrating from v15](/guide/migration-v16) - upgrade from decorators to the new API
103
+ - [Models](/guide/models) decorators, relationships, and Entity types
104
+ - [Querying](/guide/querying) operators, pagination, JSONB, and more
105
+ - [CRUD Operations](/guide/crud-operations) create, update, and destroy
106
+ - [API Reference](/reference/api) all exports and method signatures
@@ -1,19 +1,18 @@
1
1
  ---
2
- description: Create, update, and destroy records with RETURNING support, query projection, toSQL, and ON CONFLICT upserts.
2
+ description: Create, update, and destroy records with RETURNING support, query projection, and ON CONFLICT upserts.
3
3
  ---
4
4
 
5
5
  # CRUD Operations
6
6
 
7
- BigAl repositories provide `create()`, `update()`, and `destroy()` methods. All three return affected
8
- records by default (using `RETURNING *`), and all support `returnRecords` and `returnSelect` options.
9
- Results are always plain objects.
7
+ BigAl repositories provide `create()`, `update()`, and `destroy()` methods. All three return affected records
8
+ by default (using `RETURNING *`), and all support `returnRecords` and `returnSelect` options.
10
9
 
11
10
  ## Create
12
11
 
13
12
  ### Single record
14
13
 
15
14
  ```ts
16
- const product = await Product.create({
15
+ const product = await productRepository.create({
17
16
  name: 'Widget',
18
17
  priceCents: 999,
19
18
  });
@@ -23,17 +22,31 @@ const product = await Product.create({
23
22
  ### Multiple records
24
23
 
25
24
  ```ts
26
- const products = await Product.create([
25
+ const products = await productRepository.create([
27
26
  { name: 'Widget', priceCents: 999 },
28
27
  { name: 'Gadget', priceCents: 1499 },
29
28
  ]);
30
29
  // products = [{ id: 42, ... }, { id: 43, ... }]
31
30
  ```
32
31
 
32
+ Passing an array builds a single multi-row `INSERT` statement, one round trip for the whole batch. Prefer this over calling
33
+ `create()` in a loop, which issues a separate `INSERT` (and round trip) per record:
34
+
35
+ ```ts
36
+ // Correct — one INSERT statement for all rows
37
+ const products = await productRepository.create(items);
38
+
39
+ // Slower — a separate INSERT statement and round trip per item
40
+ const products = [];
41
+ for (const item of items) {
42
+ products.push(await productRepository.create(item));
43
+ }
44
+ ```
45
+
33
46
  ### Skip returning records
34
47
 
35
48
  ```ts
36
- await Product.create({ name: 'Widget', priceCents: 999 }, { returnRecords: false });
49
+ await productRepository.create({ name: 'Widget', priceCents: 999 }, { returnRecords: false });
37
50
  ```
38
51
 
39
52
  ### Query projection (returnSelect)
@@ -41,14 +54,14 @@ await Product.create({ name: 'Widget', priceCents: 999 }, { returnRecords: false
41
54
  Return only specific columns. The primary key is always included.
42
55
 
43
56
  ```ts
44
- const product = await Product.create({ name: 'Widget', priceCents: 999 }, { returnSelect: ['name'] });
57
+ const product = await productRepository.create({ name: 'Widget', priceCents: 999 }, { returnSelect: ['name'] });
45
58
  // product = { id: 42, name: 'Widget' }
46
59
  ```
47
60
 
48
61
  Pass an empty array to return only the primary key:
49
62
 
50
63
  ```ts
51
- const product = await Product.create({ name: 'Widget', priceCents: 999 }, { returnSelect: [] });
64
+ const product = await productRepository.create({ name: 'Widget', priceCents: 999 }, { returnSelect: [] });
52
65
  // product = { id: 42 }
53
66
  ```
54
67
 
@@ -59,7 +72,7 @@ Handle constraint violations with PostgreSQL's `ON CONFLICT` clause.
59
72
  ### Ignore (DO NOTHING)
60
73
 
61
74
  ```ts
62
- const product = await Product.create(
75
+ const product = await productRepository.create(
63
76
  { name: 'Widget', sku: 'WDG-001' },
64
77
  {
65
78
  onConflict: {
@@ -73,7 +86,7 @@ const product = await Product.create(
73
86
  ### Merge (DO UPDATE) - all columns
74
87
 
75
88
  ```ts
76
- const product = await Product.create(
89
+ const product = await productRepository.create(
77
90
  { name: 'Widget', sku: 'WDG-001', priceCents: 999 },
78
91
  {
79
92
  onConflict: {
@@ -87,7 +100,7 @@ const product = await Product.create(
87
100
  ### Merge - specific columns
88
101
 
89
102
  ```ts
90
- const product = await Product.create(
103
+ const product = await productRepository.create(
91
104
  { name: 'Widget', sku: 'WDG-001', priceCents: 999 },
92
105
  {
93
106
  onConflict: {
@@ -105,11 +118,11 @@ const product = await Product.create(
105
118
 
106
119
  ```ts
107
120
  // Update a single record
108
- const products = await Product.update({ id: 42 }, { name: 'Super Widget' });
121
+ const products = await productRepository.update({ id: 42 }, { name: 'Super Widget' });
109
122
  // products = [{ id: 42, name: 'Super Widget', ... }]
110
123
 
111
124
  // Update multiple records
112
- const products = await Product.update({ id: [42, 43] }, { priceCents: 1299 });
125
+ const products = await productRepository.update({ id: [42, 43] }, { priceCents: 1299 });
113
126
  // products = [{ id: 42, ... }, { id: 43, ... }]
114
127
  ```
115
128
 
@@ -118,74 +131,42 @@ const products = await Product.update({ id: [42, 43] }, { priceCents: 1299 });
118
131
  Without returning records:
119
132
 
120
133
  ```ts
121
- await Product.update({ id: 42 }, { name: 'Super Widget' }, { returnRecords: false });
134
+ await productRepository.update({ id: 42 }, { name: 'Super Widget' }, { returnRecords: false });
122
135
  ```
123
136
 
124
137
  With query projection:
125
138
 
126
139
  ```ts
127
- const products = await Product.update({ id: [42, 43] }, { priceCents: 1299 }, { returnSelect: ['id'] });
140
+ const products = await productRepository.update({ id: [42, 43] }, { priceCents: 1299 }, { returnSelect: ['id'] });
128
141
  // products = [{ id: 42 }, { id: 43 }]
129
142
  ```
130
143
 
131
144
  ## Destroy
132
145
 
133
- `destroy()` takes a where clause object. Returns `void` by default.
134
-
135
- ```ts
136
- // Delete records (returns void)
137
- await Product.destroy({ id: 42 });
138
- await Product.destroy({ id: [42, 43] });
139
- ```
140
-
141
- With returning records:
146
+ `destroy()` takes a where clause object. Returns an array of deleted records.
142
147
 
143
148
  ```ts
144
- const products = await Product.destroy({ id: 42 }, { returnRecords: true });
149
+ // Delete a single record
150
+ const products = await productRepository.destroy({ id: 42 });
145
151
  // products = [{ id: 42, name: 'Super Widget', ... }]
146
- ```
147
-
148
- With query projection:
149
152
 
150
- ```ts
151
- const products = await Product.destroy({ id: [42, 43] }, { returnSelect: ['name'] });
152
- // products = [{ id: 42, name: 'Widget' }, { id: 43, name: 'Gadget' }]
153
+ // Delete multiple records
154
+ const products = await productRepository.destroy({ id: [42, 43] });
153
155
  ```
154
156
 
155
- > The primary key is always included when returning records. Pass an empty array to return only the
156
- > primary key.
157
-
158
- ## toSQL() on mutations
157
+ > `destroy()` always returns an array, regardless of how many records were affected.
159
158
 
160
- All mutation operations support `toSQL()` to inspect the generated SQL without executing:
159
+ Without returning records:
161
160
 
162
161
  ```ts
163
- // Create
164
- const { sql, params } = Product.create({ name: 'Widget', priceCents: 999 }).toSQL();
165
-
166
- // Update
167
- const { sql, params } = Product.update({ id: 42 }, { name: 'Super Widget' }).toSQL();
168
-
169
- // Destroy
170
- const { sql, params } = Product.destroy({ id: 42 }).toSQL();
162
+ await productRepository.destroy({ id: 42 }, { returnRecords: false });
171
163
  ```
172
164
 
173
- This is useful for debugging, logging, and testing SQL generation.
174
-
175
- ## Initialization example
165
+ With query projection:
176
166
 
177
167
  ```ts
178
- import { initialize, table, serial, text, integer, createdAt, updatedAt } from 'bigal';
179
- import { Pool } from 'postgres-pool';
180
-
181
- const Product = table('products', {
182
- id: serial().primaryKey(),
183
- name: text().notNull(),
184
- priceCents: integer().notNull(),
185
- createdAt: createdAt(),
186
- updatedAt: updatedAt(),
187
- });
188
-
189
- const pool = new Pool('postgres://localhost/mydb');
190
- const { Product } = initialize({ models: { Product }, pool });
168
+ const products = await productRepository.destroy({ id: [42, 43] }, { returnSelect: ['name'] });
169
+ // products = [{ id: 42, name: 'Widget' }, { id: 43, name: 'Gadget' }]
191
170
  ```
171
+
172
+ > The primary key is always included. Pass an empty array to return only the primary key.