sqlite-zod-orm 3.19.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -44
- package/dist/index.js +13 -1
- package/package.json +1 -1
- package/src/builder.ts +30 -0
- package/src/database.ts +1 -1
- package/src/types.ts +6 -0
package/README.md
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
# sqlite-zod-orm
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/sqlite-zod-orm)
|
|
6
|
-
[](./LICENSE)
|
|
7
|
-
|
|
8
|
-
## Install
|
|
3
|
+
Type-safe SQLite ORM for Bun — Zod schemas, fluent queries, auto relationships, zero SQL.
|
|
9
4
|
|
|
10
5
|
```bash
|
|
11
6
|
bun add sqlite-zod-orm
|
|
12
7
|
```
|
|
13
8
|
|
|
14
|
-
> **Requires Bun runtime** — uses `bun:sqlite` under the hood.
|
|
15
|
-
|
|
16
9
|
## Quick Start
|
|
17
10
|
|
|
18
11
|
```typescript
|
|
@@ -22,58 +15,224 @@ const db = new Database('app.db', {
|
|
|
22
15
|
users: z.object({
|
|
23
16
|
name: z.string(),
|
|
24
17
|
email: z.string(),
|
|
25
|
-
|
|
18
|
+
score: z.number().default(0),
|
|
26
19
|
}),
|
|
20
|
+
posts: z.object({
|
|
21
|
+
title: z.string(),
|
|
22
|
+
body: z.string(),
|
|
23
|
+
userId: z.number(),
|
|
24
|
+
}),
|
|
25
|
+
}, {
|
|
26
|
+
relations: { posts: { userId: 'users' } },
|
|
27
|
+
timestamps: true,
|
|
28
|
+
softDeletes: true,
|
|
27
29
|
});
|
|
30
|
+
```
|
|
28
31
|
|
|
32
|
+
Tables are auto-created and auto-migrated from your Zod schemas. No SQL required.
|
|
33
|
+
|
|
34
|
+
## CRUD
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
29
37
|
// Insert
|
|
30
|
-
const
|
|
31
|
-
alice.id; // auto-increment ID
|
|
32
|
-
alice.role; // 'member' (from Zod default)
|
|
33
|
-
|
|
34
|
-
// Query
|
|
35
|
-
const admins = db.users.select()
|
|
36
|
-
.where({ role: 'admin' })
|
|
37
|
-
.orderBy('name')
|
|
38
|
-
.all();
|
|
38
|
+
const user = db.users.insert({ name: 'Alice', email: 'alice@co.com' });
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
|
|
40
|
+
// Read
|
|
41
|
+
const all = db.users.select().all();
|
|
42
|
+
const one = db.users.get(1);
|
|
43
|
+
const found = db.users.select().where({ name: 'Alice' }).first();
|
|
44
|
+
|
|
45
|
+
// Update (auto-persists via proxy)
|
|
46
|
+
user.score = 100; // ← saved to DB automatically
|
|
42
47
|
|
|
43
48
|
// Delete
|
|
44
|
-
db.users.delete(
|
|
49
|
+
db.users.delete(1);
|
|
50
|
+
|
|
51
|
+
// Batch
|
|
52
|
+
db.users.insertMany([{ name: 'Bob', email: 'b@co.com' }, { name: 'Charlie', email: 'c@co.com' }]);
|
|
53
|
+
db.users.upsertMany([{ id: 1, name: 'Updated Alice' }], 'id');
|
|
45
54
|
```
|
|
46
55
|
|
|
47
|
-
##
|
|
56
|
+
## Fluent Query Builder
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- **Relationships** — foreign keys, lazy navigation, fluent joins
|
|
56
|
-
- **Reactivity** — `.on('insert' | 'update' | 'delete', callback)` with trigger-based change tracking
|
|
57
|
-
- **Transactions** — `db.transaction(() => { ... })`
|
|
58
|
-
- **Timestamps** — auto `createdAt`/`updatedAt` with `{ timestamps: true }`
|
|
59
|
-
- **Soft deletes** — `{ softDeletes: true }` with `.withTrashed()`, `.onlyTrashed()`, `.restore()`
|
|
60
|
-
- **Unique constraints** — `{ unique: { users: [['email']] } }`
|
|
61
|
-
- **Schema introspection** — `db.tables()`, `db.columns('users')`
|
|
62
|
-
- **Raw SQL** — `db.raw()` / `db.exec()` escape hatch
|
|
63
|
-
- **Debug mode** — `{ debug: true }` logs all SQL to console
|
|
64
|
-
- **Distinct** — `.distinct()` on queries
|
|
65
|
-
- **Proxy queries** — SQL-like DSL with type-safe column references
|
|
58
|
+
```typescript
|
|
59
|
+
db.users.select()
|
|
60
|
+
.where({ score: { $gte: 50 } })
|
|
61
|
+
.orderBy('score', 'DESC')
|
|
62
|
+
.limit(10)
|
|
63
|
+
.all();
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
// Operators: $gt, $gte, $lt, $lte, $ne, $like, $in, $notIn, $between
|
|
66
|
+
db.users.select().where({ name: { $like: '%ali%' } }).all();
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
// whereIn / whereNotIn (array or subquery)
|
|
69
|
+
db.users.select().whereIn('id', [1, 2, 3]).all();
|
|
70
|
+
const sub = db.orders.select('userId');
|
|
71
|
+
db.users.select().whereIn('id', sub).all();
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
// Raw WHERE fragments
|
|
74
|
+
db.users.select().whereRaw('score > ? AND name != ?', [50, 'Bot']).all();
|
|
75
|
+
```
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
## Relationships
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Navigation (lazy, proxy-based)
|
|
81
|
+
const post = db.posts.get(1);
|
|
82
|
+
post.user; // → related user object
|
|
83
|
+
const user = db.users.get(1);
|
|
84
|
+
user.posts; // → array of user's posts
|
|
85
|
+
|
|
86
|
+
// Eager loading (no N+1)
|
|
87
|
+
db.posts.select().with('user').all();
|
|
75
88
|
```
|
|
76
89
|
|
|
90
|
+
## Aggregates
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
db.users.count(); // shorthand
|
|
94
|
+
db.users.select().where({ role: 'admin' }).count(); // filtered
|
|
95
|
+
db.users.select().sum('score');
|
|
96
|
+
db.users.select().avg('score');
|
|
97
|
+
db.users.select().min('score');
|
|
98
|
+
db.users.select().max('score');
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Batch Mutations
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
db.users.select().where({ role: 'guest' }).updateAll({ role: 'member' }); // → affected count
|
|
105
|
+
db.users.select().where({ role: 'spam' }).deleteAll(); // → deleted count
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Pagination
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const page = db.users.select().orderBy('name').paginate(1, 20);
|
|
112
|
+
// { data: [...], total: 42, page: 1, perPage: 20, pages: 3 }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Select Type Narrowing
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
const names = db.users.select('name', 'email').all();
|
|
119
|
+
names[0].name; // ✅ string
|
|
120
|
+
names[0].score; // ❌ TypeScript error — not selected
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Computed Getters
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const db = new Database(':memory:', { users: UserSchema }, {
|
|
127
|
+
computed: {
|
|
128
|
+
users: { fullName: (u) => `${u.first} ${u.last}` },
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
user.fullName; // 'Alice Smith' — recomputes on access
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Cascade Deletes
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const db = new Database(':memory:', { authors: AuthorSchema, books: BookSchema }, {
|
|
138
|
+
relations: { books: { author_id: 'authors' } },
|
|
139
|
+
cascade: { authors: ['books'] },
|
|
140
|
+
});
|
|
141
|
+
db.authors.delete(1); // → books with author_id=1 also deleted
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Transactions
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
db.transaction(() => {
|
|
148
|
+
db.users.insert({ name: 'Alice' });
|
|
149
|
+
db.orders.insert({ userId: 1, amount: 100 });
|
|
150
|
+
}); // auto-commits; rolls back on error
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Data Import / Export
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const backup = db.dump(); // export all tables as JSON
|
|
157
|
+
db.load(backup); // restore (truncates first)
|
|
158
|
+
db.load(backup, { append: true }); // restore without truncating
|
|
159
|
+
db.seed({ users: [{ name: 'Test User' }] }); // additive fixture seeding
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Schema Diffing
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const diff = db.diff();
|
|
166
|
+
// { users: { added: ['bio'], removed: ['legacy'], typeChanged: [] } }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Lifecycle Hooks
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const db = new Database(':memory:', { users: UserSchema }, {
|
|
173
|
+
hooks: {
|
|
174
|
+
users: {
|
|
175
|
+
beforeInsert: (data) => ({ ...data, name: data.name.trim() }),
|
|
176
|
+
afterInsert: (entity) => console.log('Created:', entity.id),
|
|
177
|
+
beforeUpdate: (id, data) => data,
|
|
178
|
+
afterUpdate: (entity) => {},
|
|
179
|
+
beforeDelete: (id) => true, // return false to cancel
|
|
180
|
+
afterDelete: (id) => {},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Soft Deletes & Timestamps
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// With softDeletes: true
|
|
190
|
+
db.users.delete(1); // sets deletedAt
|
|
191
|
+
db.users.select().all(); // excludes deleted
|
|
192
|
+
db.users.select().withTrashed().all(); // includes deleted
|
|
193
|
+
db.users.select().onlyTrashed().all(); // only deleted
|
|
194
|
+
db.users.restore(1); // un-deletes
|
|
195
|
+
|
|
196
|
+
// With timestamps: true
|
|
197
|
+
user.createdAt; // auto-set on insert
|
|
198
|
+
user.updatedAt; // auto-bumped on update
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Raw SQL
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
db.raw<User>('SELECT * FROM users WHERE score > ?', 50);
|
|
205
|
+
db.exec('UPDATE users SET score = 0 WHERE role = ?', 'guest');
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Full Feature List
|
|
209
|
+
|
|
210
|
+
- Zod-powered schema definition & runtime validation
|
|
211
|
+
- Auto table creation & migration (add columns)
|
|
212
|
+
- Fluent query builder with 10+ operators
|
|
213
|
+
- Type-safe select narrowing
|
|
214
|
+
- Relationship navigation (lazy proxy + eager loading)
|
|
215
|
+
- Soft deletes, timestamps, auto-persist proxy
|
|
216
|
+
- Lifecycle hooks (before/after insert/update/delete)
|
|
217
|
+
- Aggregates (sum, avg, min, max, count, countGrouped)
|
|
218
|
+
- Batch mutations (insertMany, upsertMany, updateAll, deleteAll, findOrCreate)
|
|
219
|
+
- Cascade deletes
|
|
220
|
+
- Computed/virtual getters
|
|
221
|
+
- Data import/export (dump, load, seed)
|
|
222
|
+
- Schema diffing
|
|
223
|
+
- Transactions
|
|
224
|
+
- Pagination
|
|
225
|
+
- whereIn/whereNotIn with subquery support
|
|
226
|
+
- JSON column auto-serialization
|
|
227
|
+
- Unique constraints
|
|
228
|
+
- Debug mode (SQL logging)
|
|
229
|
+
- Raw SQL escape hatch
|
|
230
|
+
|
|
231
|
+
## Requirements
|
|
232
|
+
|
|
233
|
+
- **Bun** ≥ 1.0 (uses `bun:sqlite` native bindings)
|
|
234
|
+
- **Zod** ≥ 3.0
|
|
235
|
+
|
|
77
236
|
## License
|
|
78
237
|
|
|
79
238
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -4572,6 +4572,17 @@ class QueryBuilder {
|
|
|
4572
4572
|
const aggSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT ${groupCols}, COUNT(*) as count FROM`);
|
|
4573
4573
|
return this.executor(aggSql, params, true);
|
|
4574
4574
|
}
|
|
4575
|
+
pluck(column) {
|
|
4576
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
4577
|
+
const pluckSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT "${column}" FROM`);
|
|
4578
|
+
const results = this.executor(pluckSql, params, true);
|
|
4579
|
+
return results.map((r) => r[column]);
|
|
4580
|
+
}
|
|
4581
|
+
clone() {
|
|
4582
|
+
const cloned = new QueryBuilder(this.tableName, this.executor, this.singleExecutor, this.joinResolver, this.conditionResolver, this.eagerLoader);
|
|
4583
|
+
cloned.iqo = JSON.parse(JSON.stringify(this.iqo));
|
|
4584
|
+
return cloned;
|
|
4585
|
+
}
|
|
4575
4586
|
updateAll(data) {
|
|
4576
4587
|
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
4577
4588
|
const whereMatch = selectSql.match(/WHERE (.+?)(?:\s+ORDER|\s+LIMIT|\s+GROUP|\s+HAVING|$)/s);
|
|
@@ -5235,7 +5246,8 @@ class _Database {
|
|
|
5235
5246
|
_pollInterval;
|
|
5236
5247
|
constructor(dbFile, schemas, options = {}) {
|
|
5237
5248
|
this.db = new SqliteDatabase(dbFile);
|
|
5238
|
-
|
|
5249
|
+
if (options.wal !== false)
|
|
5250
|
+
this.db.run("PRAGMA journal_mode = WAL");
|
|
5239
5251
|
this.db.run("PRAGMA foreign_keys = ON");
|
|
5240
5252
|
this.schemas = schemas;
|
|
5241
5253
|
this.options = options;
|
package/package.json
CHANGED
package/src/builder.ts
CHANGED
|
@@ -465,6 +465,36 @@ export class QueryBuilder<T extends Record<string, any>, TResult extends Record<
|
|
|
465
465
|
return this.executor(aggSql, params, true) as any;
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
// ---------- Convenience Methods ----------
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Return a flat array of values for a single column.
|
|
472
|
+
* ```ts
|
|
473
|
+
* db.users.select().pluck('name') // → ['Alice', 'Bob', 'Charlie']
|
|
474
|
+
* ```
|
|
475
|
+
*/
|
|
476
|
+
pluck(column: keyof T & string): any[] {
|
|
477
|
+
const { sql: selectSql, params } = compileIQO(this.tableName, this.iqo);
|
|
478
|
+
const pluckSql = selectSql.replace(/^SELECT .+? FROM/, `SELECT "${column}" FROM`);
|
|
479
|
+
const results = this.executor(pluckSql, params, true);
|
|
480
|
+
return results.map((r: any) => r[column]);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Clone this QueryBuilder so you can fork a query. */
|
|
484
|
+
clone(): QueryBuilder<T, TResult> {
|
|
485
|
+
const cloned = new QueryBuilder<T, TResult>(
|
|
486
|
+
this.tableName,
|
|
487
|
+
this.executor,
|
|
488
|
+
this.singleExecutor,
|
|
489
|
+
this.joinResolver,
|
|
490
|
+
this.conditionResolver,
|
|
491
|
+
this.eagerLoader,
|
|
492
|
+
);
|
|
493
|
+
// Deep-copy the IQO state
|
|
494
|
+
(cloned as any).iqo = JSON.parse(JSON.stringify(this.iqo));
|
|
495
|
+
return cloned;
|
|
496
|
+
}
|
|
497
|
+
|
|
468
498
|
// ---------- Batch Mutations ----------
|
|
469
499
|
|
|
470
500
|
/**
|
package/src/database.ts
CHANGED
|
@@ -65,7 +65,7 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
65
65
|
|
|
66
66
|
constructor(dbFile: string, schemas: Schemas, options: DatabaseOptions = {}) {
|
|
67
67
|
this.db = new SqliteDatabase(dbFile);
|
|
68
|
-
this.db.run('PRAGMA journal_mode = WAL');
|
|
68
|
+
if (options.wal !== false) this.db.run('PRAGMA journal_mode = WAL');
|
|
69
69
|
this.db.run('PRAGMA foreign_keys = ON');
|
|
70
70
|
this.schemas = schemas;
|
|
71
71
|
this.options = options;
|
package/src/types.ts
CHANGED
|
@@ -75,6 +75,12 @@ export type DatabaseOptions<R extends RelationsConfig = RelationsConfig> = {
|
|
|
75
75
|
* Default: `false`.
|
|
76
76
|
*/
|
|
77
77
|
debug?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Enable WAL (Write-Ahead Logging) journal mode for better
|
|
80
|
+
* concurrent read/write performance. Recommended for production.
|
|
81
|
+
* Default: `false`.
|
|
82
|
+
*/
|
|
83
|
+
wal?: boolean;
|
|
78
84
|
/**
|
|
79
85
|
* Lifecycle hooks per table. Each hook receives data and can transform it.
|
|
80
86
|
*
|