litedbmodel 0.18.0 → 0.19.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.
- package/README.md +291 -45
- package/dist/DBModel.d.ts +102 -51
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +427 -105
- package/dist/DBModel.js.map +1 -1
- package/dist/Middleware.d.ts +14 -11
- package/dist/Middleware.d.ts.map +1 -1
- package/dist/Middleware.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middlewares/statistics.d.ts +6 -6
- package/dist/middlewares/statistics.d.ts.map +1 -1
- package/dist/middlewares/statistics.js.map +1 -1
- package/dist/types.d.ts +60 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,32 +1,84 @@
|
|
|
1
1
|
# litedbmodel
|
|
2
2
|
|
|
3
|
-
A lightweight, SQL-friendly TypeScript ORM for PostgreSQL, MySQL, and SQLite.
|
|
4
|
-
|
|
5
3
|
[](https://www.npmjs.com/package/litedbmodel)
|
|
6
4
|
[](https://opensource.org/licenses/MIT)
|
|
7
5
|
|
|
6
|
+
litedbmodel is a lightweight, SQL-friendly TypeScript ORM for PostgreSQL, MySQL, and SQLite.
|
|
7
|
+
It is designed for production systems where you care about **predictable SQL**, **explicit performance control**, and **operational safety** (replication lag, N+1, accidental full scans).
|
|
8
|
+
|
|
9
|
+
|
|
8
10
|
## Philosophy
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
### SQL is not the enemy — opacity is.
|
|
13
|
+
Most ORMs hide SQL behind abstractions that are hard to debug and hard to tune.
|
|
14
|
+
litedbmodel keeps SQL visible and controllable: generated queries are intentionally simple, and complex cases use real SQL via `query()` / `execute()`.
|
|
15
|
+
|
|
16
|
+
### Make performance the default, not a post-mortem
|
|
17
|
+
- Lazy relations are supported, but **N+1 is prevented automatically** via batch loading.
|
|
18
|
+
- Per-parent limiting is done at the **SQL level** (efficient “top-N per group” patterns).
|
|
19
|
+
- Write operations default to **no RETURNING** for throughput; request PKs via `PkeyResult` only when needed.
|
|
20
|
+
|
|
21
|
+
### Production safety over convenience magic
|
|
22
|
+
- Supports **reader/writer routing** for read replicas and replication-lag-aware reads.
|
|
23
|
+
- Write operations (`create/createMany, update/updateMany, delete`) require an explicit **transaction boundary**.
|
|
24
|
+
- Configurable **hard limits** detect accidental over-fetching early.
|
|
11
25
|
|
|
12
|
-
-
|
|
13
|
-
- **
|
|
14
|
-
-
|
|
15
|
-
- **Model-Centric** — Same model serves list/detail views; relations load on-demand with automatic batch loading
|
|
26
|
+
### Refactoring-friendly, without sacrificing SQL control
|
|
27
|
+
- Column references are **symbol-based** (`Model.column`) so IDE rename/find-references work.
|
|
28
|
+
- Conditions are **type-safe tuples** (`[Column, value]`), and an ESLint plugin catches mistakes TS cannot.
|
|
16
29
|
|
|
17
30
|
> See [Design Philosophy](./docs/BENCHMARK-NESTED.md#litedbmodels-design-philosophy) for detailed comparison with query-centric ORMs.
|
|
18
31
|
|
|
19
|
-
## Features
|
|
32
|
+
## Key Features
|
|
33
|
+
|
|
34
|
+
### SQL Control & Modeling
|
|
35
|
+
- Predictable generated SQL (readable, hand-written style)
|
|
36
|
+
- Raw SQL escape hatch: `Model.query()` / `DBModel.execute()`
|
|
37
|
+
- Query-based models for complex reads (aggregations, JOINs, CTEs)
|
|
38
|
+
|
|
39
|
+
### Performance by Default
|
|
40
|
+
- Transparent N+1 prevention (automatic batch loading for lazy relations)
|
|
41
|
+
- SQL-level per-parent limit for relations
|
|
42
|
+
- Subqueries: IN/EXISTS with correlated conditions
|
|
43
|
+
|
|
44
|
+
### Operational Readiness
|
|
45
|
+
- Reader/Writer separation with sticky-writer reads after transactions (replication-lag-aware)
|
|
46
|
+
- Transactions with retry options (e.g., deadlock retry)
|
|
47
|
+
- Safety guards: `findHardLimit` / `hasManyHardLimit`
|
|
48
|
+
|
|
49
|
+
### Developer Experience
|
|
50
|
+
- Symbol-based columns + tuple conditions for refactoring safety
|
|
51
|
+
- Declarative `SKIP` pattern for optional fields/conditions
|
|
52
|
+
- Middleware for cross-cutting concerns (logging, auth, tenant isolation)
|
|
53
|
+
- Multi-database support (portable tuple API; raw SQL is dialect-dependent)
|
|
20
54
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- **
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
|
|
55
|
+
## When litedbmodel is a good fit
|
|
56
|
+
|
|
57
|
+
Choose litedbmodel if you:
|
|
58
|
+
- Build **large or high-throughput** services where SQL tuning and explain plans matter
|
|
59
|
+
- Want ORM ergonomics, but refuse to lose the ability to write/own SQL
|
|
60
|
+
- Operate with **read replicas** and care about replication lag and routing rules
|
|
61
|
+
- Need safe defaults against “oops, loaded 10M rows” and N+1 regressions
|
|
62
|
+
- Prefer a model-centric approach (list/detail + relations) with predictable behavior
|
|
63
|
+
|
|
64
|
+
## When it may NOT be a good fit
|
|
65
|
+
|
|
66
|
+
litedbmodel may be a poor fit if you:
|
|
67
|
+
- Want a “fully abstracted” ORM that hides SQL entirely
|
|
68
|
+
- Prefer a query-builder DSL as the primary interface (rather than SQL/tuple conditions)
|
|
69
|
+
- Need database-agnostic portability for complex raw SQL (dialect differences are real)
|
|
70
|
+
|
|
71
|
+
## Non-goals
|
|
72
|
+
|
|
73
|
+
Non-goals are deliberate trade-offs to keep SQL predictable and operations safe.
|
|
74
|
+
litedbmodel is intentionally **not** trying to be a “do-everything” ORM.
|
|
75
|
+
|
|
76
|
+
- **100% database-agnostic SQL**: complex queries are expected to use real SQL, and SQL dialect differences are real.
|
|
77
|
+
- **Migrations as a built-in feature**: schema migrations are out of scope (use your preferred migration tool).
|
|
78
|
+
- **Hiding SQL behind a large abstraction layer**: we prioritize predictable SQL over a fully abstracted API.
|
|
79
|
+
- **Automatic eager-loading everywhere**: relations are lazy by default; performance characteristics should stay explicit and controllable.
|
|
80
|
+
|
|
81
|
+
---
|
|
30
82
|
|
|
31
83
|
## Installation
|
|
32
84
|
|
|
@@ -66,16 +118,20 @@ DBModel.setConfig({
|
|
|
66
118
|
});
|
|
67
119
|
|
|
68
120
|
// 3. CRUD operations
|
|
69
|
-
|
|
121
|
+
await User.create([
|
|
70
122
|
[User.name, 'John'],
|
|
71
123
|
[User.email, 'john@example.com'],
|
|
72
124
|
]);
|
|
125
|
+
await User.update([[User.id, 1]], [[User.name, 'Jane']]);
|
|
126
|
+
await User.delete([[User.is_active, false]]);
|
|
73
127
|
|
|
128
|
+
// Read operations
|
|
74
129
|
const users = await User.find([[User.is_active, true]]);
|
|
75
130
|
const john = await User.findOne([[User.email, 'john@example.com']]);
|
|
76
131
|
|
|
77
|
-
|
|
78
|
-
await User.
|
|
132
|
+
// With returning: true → get PkeyResult for re-fetching
|
|
133
|
+
const created = await User.create([...], { returning: true });
|
|
134
|
+
const [newUser] = await User.findById(created);
|
|
79
135
|
```
|
|
80
136
|
|
|
81
137
|
---
|
|
@@ -159,6 +215,218 @@ Use explicit type decorators when auto-inference isn't sufficient:
|
|
|
159
215
|
|
|
160
216
|
---
|
|
161
217
|
|
|
218
|
+
## CRUD Operations
|
|
219
|
+
|
|
220
|
+
### PkeyResult Type
|
|
221
|
+
|
|
222
|
+
Write operations can optionally return a `PkeyResult` object:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
interface PkeyResult {
|
|
226
|
+
key: Column[]; // Key column(s) used to identify rows
|
|
227
|
+
values: unknown[][]; // 2D array of key values
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Single PK example
|
|
231
|
+
{ key: [User.id], values: [[1], [2], [3]] }
|
|
232
|
+
|
|
233
|
+
// Composite PK example
|
|
234
|
+
{ key: [TenantUser.tenant_id, TenantUser.id], values: [[1, 100], [1, 101]] }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Default behavior:** `returning: false` — returns `null` for better performance.
|
|
238
|
+
**With `returning: true`:** Returns `PkeyResult` with affected primary keys.
|
|
239
|
+
|
|
240
|
+
> **Note:** `PkeyResult.key` always contains primary key column(s), regardless of `keyColumns` used in `updateMany`.
|
|
241
|
+
|
|
242
|
+
### create / createMany
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// Default: returns null (no RETURNING)
|
|
246
|
+
await User.create([
|
|
247
|
+
[User.name, 'John'],
|
|
248
|
+
[User.email, 'john@example.com'],
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
// With returning: true → returns PkeyResult
|
|
252
|
+
const result = await User.create([
|
|
253
|
+
[User.name, 'John'],
|
|
254
|
+
[User.email, 'john@example.com'],
|
|
255
|
+
], { returning: true });
|
|
256
|
+
// result: { key: [User.id], values: [[1]] }
|
|
257
|
+
|
|
258
|
+
// Multiple records
|
|
259
|
+
const result = await User.createMany([
|
|
260
|
+
[[User.name, 'John'], [User.email, 'john@example.com']],
|
|
261
|
+
[[User.name, 'Jane'], [User.email, 'jane@example.com']],
|
|
262
|
+
], { returning: true });
|
|
263
|
+
// result: { key: [User.id], values: [[1], [2]] }
|
|
264
|
+
|
|
265
|
+
// Fetch created records if needed
|
|
266
|
+
const [user] = await User.findById(result);
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### update / updateMany
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// Default: returns null (no RETURNING)
|
|
273
|
+
await User.update(
|
|
274
|
+
[[User.status, 'pending']], // conditions
|
|
275
|
+
[[User.status, 'active']], // values
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// With returning: true → returns PkeyResult
|
|
279
|
+
const result = await User.update(
|
|
280
|
+
[[User.status, 'pending']],
|
|
281
|
+
[[User.status, 'active']],
|
|
282
|
+
{ returning: true }
|
|
283
|
+
);
|
|
284
|
+
// result: { key: [User.id], values: [[1], [2], [3]] }
|
|
285
|
+
|
|
286
|
+
// Bulk update with different values per row
|
|
287
|
+
const result = await User.updateMany([
|
|
288
|
+
[[User.id, 1], [User.name, 'John'], [User.email, 'john@example.com']],
|
|
289
|
+
[[User.id, 2], [User.name, 'Jane'], [User.email, 'jane@example.com']],
|
|
290
|
+
], { keyColumns: [User.id], returning: true });
|
|
291
|
+
// result: { key: [User.id], values: [[1], [2]] }
|
|
292
|
+
|
|
293
|
+
// Fetch updated records if needed
|
|
294
|
+
const users = await User.findById(result);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Generated SQL for updateMany:**
|
|
298
|
+
|
|
299
|
+
| Database | SQL |
|
|
300
|
+
|----------|-----|
|
|
301
|
+
| PostgreSQL | `UPDATE ... FROM UNNEST($1::int[], $2::text[], ...) AS v(...) WHERE t.id = v.id` |
|
|
302
|
+
| MySQL 8.0.19+ | `UPDATE ... JOIN (VALUES ROW(?, ?, ?), ...) AS v(...) ON ... SET ...` |
|
|
303
|
+
| SQLite 3.33+ | `WITH v(...) AS (VALUES (...), ...) UPDATE ... FROM v WHERE ...` |
|
|
304
|
+
|
|
305
|
+
### delete
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// Default: returns null (no RETURNING)
|
|
309
|
+
await User.delete([[User.is_active, false]]);
|
|
310
|
+
|
|
311
|
+
// With returning: true → returns PkeyResult
|
|
312
|
+
const result = await User.delete([[User.is_active, false]], { returning: true });
|
|
313
|
+
// result: { key: [User.id], values: [[4], [5]] }
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### findById
|
|
317
|
+
|
|
318
|
+
Fetch records by primary key. Accepts `PkeyResult` format for efficient batch loading:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// Single record
|
|
322
|
+
const [user] = await User.findById({ values: [[1]] });
|
|
323
|
+
|
|
324
|
+
// Multiple records
|
|
325
|
+
const users = await User.findById({ values: [[1], [2], [3]] });
|
|
326
|
+
|
|
327
|
+
// Composite PK
|
|
328
|
+
const [entry] = await TenantUser.findById({
|
|
329
|
+
values: [[1, 100]] // [tenant_id, id]
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Use with update/delete result
|
|
333
|
+
const result = await User.update(...);
|
|
334
|
+
const users = await User.findById(result);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Generated SQL:**
|
|
338
|
+
|
|
339
|
+
| Database | Single PK | Composite PK |
|
|
340
|
+
|----------|-----------|--------------|
|
|
341
|
+
| PostgreSQL | `WHERE id = ANY($1::int[])` | `WHERE (col1, col2) IN (SELECT * FROM UNNEST(...))` |
|
|
342
|
+
| MySQL | `WHERE id IN (?, ?, ?)` | `JOIN (VALUES ROW(...), ...) AS v ON ...` |
|
|
343
|
+
| SQLite | `WHERE id IN (?, ?, ?)` | `WITH v AS (VALUES ...) ... JOIN v ON ...` |
|
|
344
|
+
|
|
345
|
+
### Upsert (ON CONFLICT)
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// Insert or ignore
|
|
349
|
+
await User.create(
|
|
350
|
+
[[User.name, 'John'], [User.email, 'john@example.com']],
|
|
351
|
+
{ onConflict: User.email, onConflictIgnore: true }
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Insert or update
|
|
355
|
+
await User.create(
|
|
356
|
+
[[User.name, 'John'], [User.email, 'john@example.com']],
|
|
357
|
+
{ onConflict: User.email, onConflictUpdate: [User.name] }
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Composite unique key
|
|
361
|
+
await UserPref.create(
|
|
362
|
+
[[UserPref.user_id, 1], [UserPref.key, 'theme'], [UserPref.value, 'dark']],
|
|
363
|
+
{ onConflict: [UserPref.user_id, UserPref.key], onConflictUpdate: [UserPref.value] }
|
|
364
|
+
);
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Behavior Notes
|
|
368
|
+
|
|
369
|
+
#### PkeyResult Semantics
|
|
370
|
+
|
|
371
|
+
| Aspect | Behavior |
|
|
372
|
+
|--------|----------|
|
|
373
|
+
| **Order** | Matches database `RETURNING` order (not guaranteed across DBs; `findById(result)` order is also unspecified) |
|
|
374
|
+
| **update result** | Contains PKs of **matched rows** (rows matching WHERE condition, regardless of whether values actually changed) |
|
|
375
|
+
| **delete result** | Contains PKs of **deleted rows** |
|
|
376
|
+
| **Duplicates** | No duplicates (each row appears once; MySQL pre-SELECT uses `DISTINCT`) |
|
|
377
|
+
| **Empty result** | `{ key: [...], values: [] }` when no rows affected (not `null`) |
|
|
378
|
+
|
|
379
|
+
> **Note:** For MySQL (no `RETURNING`), when `returning: true`:
|
|
380
|
+
> - `update`/`delete`: Executes pre-SELECT (with `DISTINCT`) to get PKs, then executes the operation (2 queries in same transaction)
|
|
381
|
+
> - `updateMany`: Executes update, then SELECT to get PKs of affected rows (2 queries in same transaction)
|
|
382
|
+
> - When `returning: false` (default): Single query, returns `null`
|
|
383
|
+
|
|
384
|
+
#### Batch Limits
|
|
385
|
+
|
|
386
|
+
`createMany` and `updateMany` do **not** auto-split large batches. Users are responsible for chunking:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// Recommended: chunk large batches (DB-dependent limits)
|
|
390
|
+
const BATCH_SIZE = 1000; // Adjust based on your DB and row size
|
|
391
|
+
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
392
|
+
const chunk = rows.slice(i, i + BATCH_SIZE);
|
|
393
|
+
await User.updateMany(chunk, { keyColumns: [User.id] });
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
| Database | Practical Limits |
|
|
398
|
+
|----------|------------------|
|
|
399
|
+
| PostgreSQL | ~32,767 parameters per query |
|
|
400
|
+
| MySQL | `max_allowed_packet` (default 64MB), ~65,535 placeholders |
|
|
401
|
+
| SQLite | 999 variables (compile-time `SQLITE_MAX_VARIABLE_NUMBER`) |
|
|
402
|
+
|
|
403
|
+
#### updateMany keyColumns Contract
|
|
404
|
+
|
|
405
|
+
| Requirement | Description |
|
|
406
|
+
|-------------|-------------|
|
|
407
|
+
| **Must be unique** | `keyColumns` must uniquely identify rows (primary key or unique constraint) |
|
|
408
|
+
| **Must exist in rows** | Every row must include all `keyColumns` |
|
|
409
|
+
| **Non-key columns** | Columns not in `keyColumns` become `SET` clause values |
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// ✅ Valid: keyColumns is primary key
|
|
413
|
+
await User.updateMany([
|
|
414
|
+
[[User.id, 1], [User.name, 'John']],
|
|
415
|
+
], { keyColumns: [User.id] });
|
|
416
|
+
|
|
417
|
+
// ✅ Valid: keyColumns is unique constraint
|
|
418
|
+
await User.updateMany([
|
|
419
|
+
[[User.email, 'john@example.com'], [User.name, 'John']],
|
|
420
|
+
], { keyColumns: [User.email] }); // If email has UNIQUE constraint
|
|
421
|
+
|
|
422
|
+
// ❌ Invalid: keyColumns missing from row
|
|
423
|
+
await User.updateMany([
|
|
424
|
+
[[User.name, 'John']], // Missing User.id!
|
|
425
|
+
], { keyColumns: [User.id] });
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
162
430
|
## Type-Safe Conditions
|
|
163
431
|
|
|
164
432
|
Conditions use `[Column, value]` tuples for compile-time validation. For operators, use `${Model.column}` in template literals—the ESLint plugin catches incorrect column references.
|
|
@@ -291,30 +559,6 @@ await User.find([
|
|
|
291
559
|
|
|
292
560
|
---
|
|
293
561
|
|
|
294
|
-
## Upsert (ON CONFLICT)
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
// Insert or ignore
|
|
298
|
-
await User.create(
|
|
299
|
-
[[User.name, 'John'], [User.email, 'john@example.com']],
|
|
300
|
-
{ onConflict: User.email, onConflictIgnore: true }
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
// Insert or update
|
|
304
|
-
await User.create(
|
|
305
|
-
[[User.name, 'John'], [User.email, 'john@example.com']],
|
|
306
|
-
{ onConflict: User.email, onConflictUpdate: [User.name] }
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
// Composite unique key
|
|
310
|
-
await UserPref.create(
|
|
311
|
-
[[UserPref.user_id, 1], [UserPref.key, 'theme'], [UserPref.value, 'dark']],
|
|
312
|
-
{ onConflict: [UserPref.user_id, UserPref.key], onConflictUpdate: [UserPref.value] }
|
|
313
|
-
);
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
---
|
|
317
|
-
|
|
318
562
|
## Relation Decorators
|
|
319
563
|
|
|
320
564
|
Define relations declaratively with type-safe decorators:
|
|
@@ -514,6 +758,7 @@ flowchart TD
|
|
|
514
758
|
create["create()"]
|
|
515
759
|
createMany["createMany()"]
|
|
516
760
|
update["update()"]
|
|
761
|
+
updateMany["updateMany()"]
|
|
517
762
|
delete["delete()"]
|
|
518
763
|
end
|
|
519
764
|
|
|
@@ -547,6 +792,7 @@ flowchart TD
|
|
|
547
792
|
create --> execute
|
|
548
793
|
createMany --> execute
|
|
549
794
|
update --> execute
|
|
795
|
+
updateMany --> execute
|
|
550
796
|
delete --> execute
|
|
551
797
|
|
|
552
798
|
query --> execute
|
|
@@ -556,7 +802,7 @@ flowchart TD
|
|
|
556
802
|
```
|
|
557
803
|
|
|
558
804
|
**Middleware hooks:**
|
|
559
|
-
- **Method-level**: `find`, `findOne`, `findById`, `count`, `create`, `createMany`, `update`, `delete`
|
|
805
|
+
- **Method-level**: `find`, `findOne`, `findById`, `count`, `create`, `createMany`, `update`, `updateMany`, `delete`
|
|
560
806
|
- **Instantiation-level**: `query` — returns model instances from raw SQL
|
|
561
807
|
- **SQL-level**: `execute` — intercepts ALL SQL queries (SELECT, INSERT, UPDATE, DELETE)
|
|
562
808
|
|
package/dist/DBModel.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { DBBoolValue, DBNullValue, DBNotNullValue, DBImmediateValue, DBSubquery, DBExists } from './DBValues';
|
|
5
5
|
import { type ConditionObject } from './DBConditions';
|
|
6
6
|
import { DBHandler, type DBConfig, type DBConnection } from './DBHandler';
|
|
7
|
-
import type { SelectOptions, InsertOptions, UpdateOptions, DeleteOptions, TransactionOptions, LimitConfig, DBConfigOptions } from './types';
|
|
7
|
+
import type { SelectOptions, InsertOptions, UpdateOptions, DeleteOptions, UpdateManyOptions, TransactionOptions, LimitConfig, DBConfigOptions, PkeyResult } from './types';
|
|
8
8
|
import { type Column, type OrderSpec, type CVs, type Conds, type CondsOf, type OrCondOf, type ColumnsOf } from './Column';
|
|
9
9
|
import type { MiddlewareClass, ExecuteResult } from './Middleware';
|
|
10
10
|
import { type KeyPair, type CompositeKeyPairs } from './decorators';
|
|
@@ -305,6 +305,11 @@ export declare abstract class DBModel {
|
|
|
305
305
|
* @internal
|
|
306
306
|
*/
|
|
307
307
|
protected static _createInstance<T extends typeof DBModel>(this: T, row: Record<string, unknown>): InstanceType<T>;
|
|
308
|
+
/**
|
|
309
|
+
* Build PkeyResult from rows containing primary key values
|
|
310
|
+
* @internal
|
|
311
|
+
*/
|
|
312
|
+
protected static _buildPkeyResult(rows: Record<string, unknown>[], pkeyColumns?: Column[]): PkeyResult;
|
|
308
313
|
/** Boolean TRUE value */
|
|
309
314
|
static readonly true: DBBoolValue;
|
|
310
315
|
/** Boolean FALSE value */
|
|
@@ -694,18 +699,32 @@ export declare abstract class DBModel {
|
|
|
694
699
|
*/
|
|
695
700
|
static findOne<T extends typeof DBModel>(this: T, conditions: CondsOf<T>, options?: SelectOptions): Promise<InstanceType<T> | null>;
|
|
696
701
|
/**
|
|
697
|
-
* Find
|
|
698
|
-
*
|
|
702
|
+
* Find records by primary key using PkeyResult format.
|
|
703
|
+
* Efficiently fetches multiple records by their primary keys.
|
|
704
|
+
*
|
|
705
|
+
* @param pkeyResult - Object with `values` property containing 2D array of PK values
|
|
699
706
|
* @param options - Query options
|
|
700
|
-
* @returns
|
|
707
|
+
* @returns Array of model instances (empty array if no matches)
|
|
701
708
|
*
|
|
702
709
|
* @example
|
|
703
710
|
* ```typescript
|
|
704
|
-
*
|
|
705
|
-
* const
|
|
711
|
+
* // Single record
|
|
712
|
+
* const [user] = await User.findById({ values: [[1]] });
|
|
713
|
+
*
|
|
714
|
+
* // Multiple records
|
|
715
|
+
* const users = await User.findById({ values: [[1], [2], [3]] });
|
|
716
|
+
*
|
|
717
|
+
* // Composite PK
|
|
718
|
+
* const [entry] = await TenantUser.findById({
|
|
719
|
+
* values: [[1, 100]] // [tenant_id, id]
|
|
720
|
+
* });
|
|
721
|
+
*
|
|
722
|
+
* // Use with write operation result
|
|
723
|
+
* const result = await User.update(..., { returning: true });
|
|
724
|
+
* const users = await User.findById(result);
|
|
706
725
|
* ```
|
|
707
726
|
*/
|
|
708
|
-
static findById<T extends typeof DBModel>(this: T,
|
|
727
|
+
static findById<T extends typeof DBModel>(this: T, pkeyResult: Pick<PkeyResult, 'values'>, options?: SelectOptions): Promise<InstanceType<T>[]>;
|
|
709
728
|
/**
|
|
710
729
|
* Count records using type-safe condition tuples.
|
|
711
730
|
*
|
|
@@ -723,68 +742,124 @@ export declare abstract class DBModel {
|
|
|
723
742
|
* Value types are validated at compile time.
|
|
724
743
|
*
|
|
725
744
|
* @param pairs - Array of [Column, value] tuples
|
|
726
|
-
* @param options - Insert options
|
|
727
|
-
* @returns
|
|
745
|
+
* @param options - Insert options (returning: true to get PkeyResult)
|
|
746
|
+
* @returns null by default, or PkeyResult if returning: true
|
|
728
747
|
*
|
|
729
748
|
* @example
|
|
730
749
|
* ```typescript
|
|
731
|
-
*
|
|
750
|
+
* // Default: returns null
|
|
751
|
+
* await User.create([
|
|
732
752
|
* [User.name, 'John'],
|
|
733
753
|
* [User.email, 'john@test.com'],
|
|
734
|
-
* [User.is_active, true],
|
|
735
754
|
* ]);
|
|
755
|
+
*
|
|
756
|
+
* // With returning: true → PkeyResult
|
|
757
|
+
* const result = await User.create([
|
|
758
|
+
* [User.name, 'John'],
|
|
759
|
+
* [User.email, 'john@test.com'],
|
|
760
|
+
* ], { returning: true });
|
|
761
|
+
* const [user] = await User.findById(result);
|
|
736
762
|
* ```
|
|
737
763
|
*/
|
|
738
|
-
static create<T extends typeof DBModel, P extends readonly (readonly [Column<any, any>, any])[]>(this: T, pairs: P & CVs<P>, options?: InsertOptions<InstanceType<T>>): Promise<
|
|
764
|
+
static create<T extends typeof DBModel, P extends readonly (readonly [Column<any, any>, any])[]>(this: T, pairs: P & CVs<P>, options?: InsertOptions<InstanceType<T>>): Promise<PkeyResult | null>;
|
|
739
765
|
/**
|
|
740
766
|
* Create multiple records using type-safe column-value tuples.
|
|
741
767
|
*
|
|
742
768
|
* @param pairsArray - Array of tuple arrays
|
|
743
|
-
* @param options - Insert options
|
|
744
|
-
* @returns
|
|
769
|
+
* @param options - Insert options (returning: true to get PkeyResult)
|
|
770
|
+
* @returns null by default, or PkeyResult if returning: true
|
|
745
771
|
*
|
|
746
772
|
* @example
|
|
747
773
|
* ```typescript
|
|
748
|
-
*
|
|
774
|
+
* // Default: returns null
|
|
775
|
+
* await User.createMany([
|
|
749
776
|
* [[User.name, 'John'], [User.email, 'john@test.com']],
|
|
750
777
|
* [[User.name, 'Jane'], [User.email, 'jane@test.com']],
|
|
751
778
|
* ]);
|
|
779
|
+
*
|
|
780
|
+
* // With returning: true → PkeyResult
|
|
781
|
+
* const result = await User.createMany([
|
|
782
|
+
* [[User.name, 'John'], [User.email, 'john@test.com']],
|
|
783
|
+
* [[User.name, 'Jane'], [User.email, 'jane@test.com']],
|
|
784
|
+
* ], { returning: true });
|
|
785
|
+
* const users = await User.findById(result);
|
|
752
786
|
* ```
|
|
753
787
|
*/
|
|
754
|
-
static createMany<T extends typeof DBModel>(this: T, pairsArray: readonly (readonly (readonly [Column<any, any>, any])[])[], options?: InsertOptions<InstanceType<T>>): Promise<
|
|
788
|
+
static createMany<T extends typeof DBModel>(this: T, pairsArray: readonly (readonly (readonly [Column<any, any>, any])[])[], options?: InsertOptions<InstanceType<T>>): Promise<PkeyResult | null>;
|
|
755
789
|
/**
|
|
756
790
|
* Update records using type-safe column-value tuples.
|
|
757
791
|
* Value types are validated at compile time.
|
|
758
792
|
*
|
|
759
793
|
* @param conditions - Array of condition tuples for WHERE clause
|
|
760
794
|
* @param values - Array of [Column, value] tuples for SET clause
|
|
761
|
-
* @param options - Update options
|
|
762
|
-
* @returns
|
|
795
|
+
* @param options - Update options (returning: true to get PkeyResult)
|
|
796
|
+
* @returns null by default, or PkeyResult if returning: true
|
|
763
797
|
*
|
|
764
798
|
* @example
|
|
765
799
|
* ```typescript
|
|
800
|
+
* // Default: returns null
|
|
766
801
|
* await User.update(
|
|
767
|
-
* [[User.id, 1]],
|
|
768
|
-
* [
|
|
769
|
-
* [User.name, 'Jane'],
|
|
770
|
-
* [User.email, 'jane@test.com'],
|
|
771
|
-
* ],
|
|
802
|
+
* [[User.id, 1]],
|
|
803
|
+
* [[User.name, 'Jane']],
|
|
772
804
|
* );
|
|
805
|
+
*
|
|
806
|
+
* // With returning: true → PkeyResult
|
|
807
|
+
* const result = await User.update(
|
|
808
|
+
* [[User.status, 'pending']],
|
|
809
|
+
* [[User.status, 'active']],
|
|
810
|
+
* { returning: true }
|
|
811
|
+
* );
|
|
812
|
+
* const users = await User.findById(result);
|
|
773
813
|
* ```
|
|
774
814
|
*/
|
|
775
|
-
static update<T extends typeof DBModel, V extends readonly (readonly [Column<any, any>, any])[]>(this: T, conditions: CondsOf<T>, values: V & CVs<V>, options?: UpdateOptions): Promise<
|
|
815
|
+
static update<T extends typeof DBModel, V extends readonly (readonly [Column<any, any>, any])[]>(this: T, conditions: CondsOf<T>, values: V & CVs<V>, options?: UpdateOptions): Promise<PkeyResult | null>;
|
|
776
816
|
/**
|
|
777
817
|
* Delete records matching conditions
|
|
778
818
|
* @param conditions - Filter conditions
|
|
779
|
-
* @param options - Delete options
|
|
780
|
-
* @returns
|
|
819
|
+
* @param options - Delete options (returning: true to get PkeyResult)
|
|
820
|
+
* @returns null by default, or PkeyResult if returning: true
|
|
781
821
|
*
|
|
782
822
|
* @example
|
|
783
823
|
* ```typescript
|
|
824
|
+
* // Default: returns null
|
|
784
825
|
* await User.delete([[User.is_active, false]]);
|
|
826
|
+
*
|
|
827
|
+
* // With returning: true → PkeyResult
|
|
828
|
+
* const result = await User.delete([[User.is_active, false]], { returning: true });
|
|
829
|
+
* // result: { key: [User.id], values: [[4], [5]] }
|
|
785
830
|
* ```
|
|
786
831
|
*/
|
|
787
|
-
static delete<T extends typeof DBModel>(this: T, conditions: CondsOf<T>, options?: DeleteOptions): Promise<
|
|
832
|
+
static delete<T extends typeof DBModel>(this: T, conditions: CondsOf<T>, options?: DeleteOptions): Promise<PkeyResult | null>;
|
|
833
|
+
/**
|
|
834
|
+
* Update multiple records with different values per row.
|
|
835
|
+
* Uses efficient bulk update strategies (UNNEST for PostgreSQL, VALUES for MySQL/SQLite).
|
|
836
|
+
*
|
|
837
|
+
* @param rows - Array of [Column, value][] tuples, each representing one row's values
|
|
838
|
+
* @param options - Options including keyColumns to identify rows
|
|
839
|
+
* @returns null by default, or PkeyResult if returning: true
|
|
840
|
+
*
|
|
841
|
+
* @example
|
|
842
|
+
* ```typescript
|
|
843
|
+
* // Default: returns null
|
|
844
|
+
* await User.updateMany([
|
|
845
|
+
* [[User.id, 1], [User.name, 'John'], [User.email, 'john@example.com']],
|
|
846
|
+
* [[User.id, 2], [User.name, 'Jane'], [User.email, 'jane@example.com']],
|
|
847
|
+
* ], { keyColumns: [User.id] });
|
|
848
|
+
*
|
|
849
|
+
* // With returning: true → PkeyResult
|
|
850
|
+
* const result = await User.updateMany([
|
|
851
|
+
* [[User.id, 1], [User.name, 'John']],
|
|
852
|
+
* [[User.id, 2], [User.name, 'Jane']],
|
|
853
|
+
* ], { keyColumns: [User.id], returning: true });
|
|
854
|
+
* const users = await User.findById(result);
|
|
855
|
+
* ```
|
|
856
|
+
*/
|
|
857
|
+
static updateMany<T extends typeof DBModel>(this: T, rows: readonly (readonly (readonly [Column<any, any>, any])[])[], options: UpdateManyOptions): Promise<PkeyResult | null>;
|
|
858
|
+
/**
|
|
859
|
+
* Infer PostgreSQL type from JavaScript value
|
|
860
|
+
* @internal
|
|
861
|
+
*/
|
|
862
|
+
private static _inferPgType;
|
|
788
863
|
/**
|
|
789
864
|
* Execute raw SQL query.
|
|
790
865
|
*
|
|
@@ -958,30 +1033,6 @@ export declare abstract class DBModel {
|
|
|
958
1033
|
* ```
|
|
959
1034
|
*/
|
|
960
1035
|
static createDBBase(config: DBConfig, options?: DBConfigOptions): typeof DBModel;
|
|
961
|
-
/**
|
|
962
|
-
* Save this instance (INSERT if new, UPDATE if exists)
|
|
963
|
-
* @param properties - Properties to save (uses all properties if not specified)
|
|
964
|
-
* @returns true if successful
|
|
965
|
-
*
|
|
966
|
-
* @example
|
|
967
|
-
* ```typescript
|
|
968
|
-
* const user = new User();
|
|
969
|
-
* user.name = 'John';
|
|
970
|
-
* user.email = 'john@example.com';
|
|
971
|
-
* await user.save();
|
|
972
|
-
* ```
|
|
973
|
-
*/
|
|
974
|
-
save(properties?: Record<string, unknown>): Promise<boolean>;
|
|
975
|
-
/**
|
|
976
|
-
* Delete this instance from the database
|
|
977
|
-
* @returns true if successful
|
|
978
|
-
*
|
|
979
|
-
* @example
|
|
980
|
-
* ```typescript
|
|
981
|
-
* await user.destroy();
|
|
982
|
-
* ```
|
|
983
|
-
*/
|
|
984
|
-
destroy(): Promise<boolean>;
|
|
985
1036
|
/**
|
|
986
1037
|
* Reload this instance from the database
|
|
987
1038
|
* @param forUpdate - If true, lock the row for update
|