sqlite-zod-orm 3.0.0 → 3.2.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 +181 -223
- package/dist/index.js +5010 -0
- package/package.json +13 -13
- package/src/build.ts +8 -10
- package/src/database.ts +491 -0
- package/src/index.ts +24 -0
- package/src/proxy-query.ts +55 -51
- package/src/query-builder.ts +145 -6
- package/src/schema.ts +122 -0
- package/src/types.ts +195 -0
- package/src/satidb.ts +0 -1153
package/README.md
CHANGED
|
@@ -1,314 +1,272 @@
|
|
|
1
|
-
#
|
|
1
|
+
# sqlite-zod-orm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe SQLite ORM for Bun. Define schemas with Zod, get a fully-typed database with automatic relationships, lazy navigation, and zero SQL.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- **📊 Type-Safe Schemas**: Powered by Zod for runtime validation
|
|
9
|
-
- **🔗 Automatic Relationships**: Belongs-to, one-to-many, many-to-many support
|
|
10
|
-
- **📺 Event Subscriptions**: Listen to insert/update/delete events
|
|
11
|
-
- **🏗️ Explicit Junction Tables**: Full control over many-to-many relationships
|
|
12
|
-
- **🚀 Fluent API**: Intuitive, chainable operations
|
|
13
|
-
- **⚡ Zero-Config**: Works with Bun SQLite out of the box
|
|
5
|
+
```bash
|
|
6
|
+
bun add sqlite-zod-orm
|
|
7
|
+
```
|
|
14
8
|
|
|
15
|
-
##
|
|
9
|
+
## Quick Start
|
|
16
10
|
|
|
17
11
|
```typescript
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const PostSchema = z.object({
|
|
27
|
-
title: z.string(),
|
|
28
|
-
content: z.string(),
|
|
29
|
-
author: z.lazy(() => AuthorSchema).optional(),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Create database
|
|
33
|
-
const db = new MyDatabase(':memory:', {
|
|
34
|
-
authors: AuthorSchema,
|
|
35
|
-
posts: PostSchema,
|
|
12
|
+
import { Database, z } from 'sqlite-zod-orm';
|
|
13
|
+
|
|
14
|
+
const db = new Database(':memory:', {
|
|
15
|
+
users: z.object({
|
|
16
|
+
name: z.string(),
|
|
17
|
+
email: z.string().email(),
|
|
18
|
+
role: z.string().default('member'),
|
|
19
|
+
}),
|
|
36
20
|
});
|
|
37
21
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
// Reactive editing - just change properties!
|
|
43
|
-
post.title = 'Hello TypeScript'; // Automatically saved to DB
|
|
22
|
+
const alice = db.users.insert({ name: 'Alice', email: 'alice@example.com', role: 'admin' });
|
|
23
|
+
const admin = db.users.select().where({ role: 'admin' }).get(); // single row
|
|
24
|
+
const all = db.users.select().all(); // all rows
|
|
44
25
|
```
|
|
45
26
|
|
|
46
|
-
|
|
27
|
+
---
|
|
47
28
|
|
|
48
|
-
|
|
29
|
+
## Defining Relationships
|
|
49
30
|
|
|
50
|
-
|
|
31
|
+
FK columns go in your schema. The `relations` config declares which FK points to which table:
|
|
51
32
|
|
|
52
33
|
```typescript
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
name: z.string(),
|
|
56
|
-
posts: z.lazy(() => z.array(PostSchema)).optional(), // one-to-many
|
|
57
|
-
});
|
|
34
|
+
const AuthorSchema = z.object({ name: z.string(), country: z.string() });
|
|
35
|
+
const BookSchema = z.object({ title: z.string(), year: z.number(), author_id: z.number().optional() });
|
|
58
36
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
37
|
+
const db = new Database(':memory:', {
|
|
38
|
+
authors: AuthorSchema,
|
|
39
|
+
books: BookSchema,
|
|
40
|
+
}, {
|
|
41
|
+
relations: {
|
|
42
|
+
books: { author_id: 'authors' },
|
|
43
|
+
},
|
|
63
44
|
});
|
|
64
45
|
```
|
|
65
46
|
|
|
66
|
-
|
|
47
|
+
`books: { author_id: 'authors' }` tells the ORM that `books.author_id` is a foreign key referencing `authors.id`. The ORM automatically:
|
|
67
48
|
|
|
68
|
-
|
|
49
|
+
- Adds `FOREIGN KEY (author_id) REFERENCES authors(id)` constraint
|
|
50
|
+
- Infers the inverse one-to-many `authors → books`
|
|
51
|
+
- Enables lazy navigation: `book.author()` and `author.books()`
|
|
52
|
+
- Enables fluent joins: `db.books.select().join(db.authors).all()`
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
// Many-to-many with rich junction table
|
|
72
|
-
const PostTagSchema = z.object({
|
|
73
|
-
post: z.lazy(() => PostSchema).optional(),
|
|
74
|
-
tag: z.lazy(() => TagSchema).optional(),
|
|
75
|
-
appliedAt: z.date().default(() => new Date()),
|
|
76
|
-
appliedBy: z.string(),
|
|
77
|
-
priority: z.number().default(0),
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Usage
|
|
81
|
-
const postTag = post.postTags.push({
|
|
82
|
-
tagId: tag.id,
|
|
83
|
-
appliedBy: 'editor',
|
|
84
|
-
priority: 5
|
|
85
|
-
});
|
|
86
|
-
```
|
|
54
|
+
The nav method name is derived by stripping `_id` from the FK column: `author_id` → `author()`.
|
|
87
55
|
|
|
88
|
-
|
|
89
|
-
- Full control over additional fields (timestamps, metadata, etc.)
|
|
90
|
-
- Junction tables are queryable entities themselves
|
|
91
|
-
- More flexible than auto-generated tables
|
|
92
|
-
- Clearer data modeling
|
|
56
|
+
---
|
|
93
57
|
|
|
94
|
-
##
|
|
58
|
+
## Querying — `select()` is the only path
|
|
95
59
|
|
|
96
|
-
|
|
60
|
+
All queries go through `select()`:
|
|
97
61
|
|
|
98
62
|
```typescript
|
|
99
|
-
//
|
|
100
|
-
const user = db.users.
|
|
101
|
-
|
|
102
|
-
// Find multiple with conditions
|
|
103
|
-
const activeUsers = db.users.find({ active: true, $limit: 10 });
|
|
104
|
-
|
|
105
|
-
// Get single entity
|
|
106
|
-
const user = db.users.get({ email: 'alice@example.com' });
|
|
107
|
-
const userById = db.users.get('user-id-123');
|
|
63
|
+
// Single row
|
|
64
|
+
const user = db.users.select().where({ id: 1 }).get();
|
|
108
65
|
|
|
109
|
-
//
|
|
110
|
-
const
|
|
66
|
+
// All matching rows
|
|
67
|
+
const admins = db.users.select().where({ role: 'admin' }).all();
|
|
111
68
|
|
|
112
|
-
//
|
|
113
|
-
const
|
|
69
|
+
// All rows
|
|
70
|
+
const everyone = db.users.select().all();
|
|
114
71
|
|
|
115
|
-
//
|
|
116
|
-
db.users.
|
|
72
|
+
// Count
|
|
73
|
+
const count = db.users.select().count();
|
|
117
74
|
```
|
|
118
75
|
|
|
119
|
-
###
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
// One-to-many: Add related entities
|
|
123
|
-
const post = author.posts.push({ title: 'New Post', content: '...' });
|
|
124
|
-
|
|
125
|
-
// Query related entities
|
|
126
|
-
const authorPosts = author.posts(); // All posts
|
|
127
|
-
const recentPosts = author.posts({ $limit: 5, $sortBy: 'createdAt:desc' });
|
|
76
|
+
### Operators
|
|
128
77
|
|
|
129
|
-
|
|
130
|
-
const postAuthor = post.author();
|
|
78
|
+
`$gt` `$gte` `$lt` `$lte` `$ne` `$in`
|
|
131
79
|
|
|
132
|
-
|
|
133
|
-
const
|
|
80
|
+
```typescript
|
|
81
|
+
const topScorers = db.users.select()
|
|
82
|
+
.where({ score: { $gt: 50 } })
|
|
83
|
+
.orderBy('score', 'desc')
|
|
84
|
+
.limit(10)
|
|
85
|
+
.all();
|
|
134
86
|
```
|
|
135
87
|
|
|
136
|
-
###
|
|
88
|
+
### `$or`
|
|
137
89
|
|
|
138
90
|
```typescript
|
|
139
|
-
const
|
|
91
|
+
const results = db.users.select()
|
|
92
|
+
.where({ $or: [{ role: 'admin' }, { score: { $gt: 50 } }] })
|
|
93
|
+
.all();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Fluent Join
|
|
140
97
|
|
|
141
|
-
|
|
142
|
-
user.name = 'New Name';
|
|
143
|
-
user.email = 'new@email.com';
|
|
144
|
-
user.active = false;
|
|
98
|
+
Auto-infers foreign keys from relationships:
|
|
145
99
|
|
|
146
|
-
|
|
147
|
-
|
|
100
|
+
```typescript
|
|
101
|
+
const rows = db.books.select('title', 'year')
|
|
102
|
+
.join(db.authors, ['name', 'country'])
|
|
103
|
+
.where({ year: { $gt: 1800 } })
|
|
104
|
+
.orderBy('year', 'asc')
|
|
105
|
+
.all();
|
|
106
|
+
// → [{ title: 'War and Peace', year: 1869, authors_name: 'Leo Tolstoy', ... }]
|
|
148
107
|
```
|
|
149
108
|
|
|
150
|
-
###
|
|
109
|
+
### `db.query()` — Proxy Query (SQL-like)
|
|
110
|
+
|
|
111
|
+
Full SQL-like control with destructured table aliases:
|
|
151
112
|
|
|
152
113
|
```typescript
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
114
|
+
const rows = db.query(c => {
|
|
115
|
+
const { authors: a, books: b } = c;
|
|
116
|
+
return {
|
|
117
|
+
select: { author: a.name, book: b.title, year: b.year },
|
|
118
|
+
join: [[b.author_id, a.id]],
|
|
119
|
+
where: { [a.country]: 'Russia' },
|
|
120
|
+
orderBy: { [b.year]: 'asc' },
|
|
121
|
+
};
|
|
156
122
|
});
|
|
123
|
+
```
|
|
157
124
|
|
|
158
|
-
|
|
159
|
-
console.log(`Post updated: ${post.title}`);
|
|
160
|
-
});
|
|
125
|
+
---
|
|
161
126
|
|
|
162
|
-
|
|
163
|
-
console.log(`User deleted: ${user.name}`);
|
|
164
|
-
});
|
|
165
|
-
```
|
|
127
|
+
## Lazy Navigation
|
|
166
128
|
|
|
167
|
-
|
|
129
|
+
Relationship fields become callable methods on entities. The method name is the FK column with `_id` stripped:
|
|
168
130
|
|
|
169
131
|
```typescript
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
132
|
+
// belongs-to: book.author_id → book.author()
|
|
133
|
+
const book = db.books.select().where({ title: 'War and Peace' }).get()!;
|
|
134
|
+
const author = book.author(); // → { name: 'Leo Tolstoy', ... }
|
|
135
|
+
|
|
136
|
+
// one-to-many: author → books
|
|
137
|
+
const books = tolstoy.books(); // → [{ title: 'War and Peace' }, ...]
|
|
138
|
+
|
|
139
|
+
// Chain
|
|
140
|
+
const allByAuthor = book.author().books();
|
|
176
141
|
```
|
|
177
142
|
|
|
178
|
-
|
|
143
|
+
---
|
|
179
144
|
|
|
180
|
-
|
|
145
|
+
## CRUD
|
|
181
146
|
|
|
182
147
|
```typescript
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
chats: z.lazy(() => z.array(ChatSchema)).optional(),
|
|
186
|
-
});
|
|
148
|
+
// Insert (defaults fill in automatically)
|
|
149
|
+
const user = db.users.insert({ name: 'Alice', role: 'admin' });
|
|
187
150
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
personality: z.lazy(() => PersonalitySchema).optional(),
|
|
191
|
-
});
|
|
151
|
+
// Insert with FK
|
|
152
|
+
const book = db.books.insert({ title: 'War and Peace', year: 1869, author_id: tolstoy.id });
|
|
192
153
|
|
|
193
|
-
//
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
154
|
+
// Read
|
|
155
|
+
const one = db.users.select().where({ id: 1 }).get();
|
|
156
|
+
const some = db.users.select().where({ role: 'admin' }).all();
|
|
157
|
+
const all = db.users.select().all();
|
|
158
|
+
const count = db.users.select().count();
|
|
198
159
|
|
|
199
|
-
|
|
160
|
+
// Entity-level update
|
|
161
|
+
user.update({ role: 'superadmin' });
|
|
200
162
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const UserSchema = z.object({
|
|
204
|
-
name: z.string(),
|
|
205
|
-
likes: z.lazy(() => z.array(LikeSchema)).optional(),
|
|
206
|
-
});
|
|
163
|
+
// Update by ID
|
|
164
|
+
db.users.update(1, { role: 'superadmin' });
|
|
207
165
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
likes: z.lazy(() => z.array(LikeSchema)).optional(),
|
|
211
|
-
});
|
|
166
|
+
// Fluent update with WHERE
|
|
167
|
+
db.users.update({ role: 'member' }).where({ role: 'guest' }).exec();
|
|
212
168
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
user: z.lazy(() => UserSchema).optional(),
|
|
216
|
-
product: z.lazy(() => ProductSchema).optional(),
|
|
217
|
-
rating: z.number().min(1).max(5),
|
|
218
|
-
likedAt: z.date().default(() => new Date()),
|
|
219
|
-
});
|
|
169
|
+
// Upsert
|
|
170
|
+
db.users.upsert({ name: 'Alice' }, { name: 'Alice', role: 'admin' });
|
|
220
171
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
172
|
+
// Delete
|
|
173
|
+
db.users.delete(1);
|
|
174
|
+
```
|
|
224
175
|
|
|
225
|
-
|
|
226
|
-
const like = user.likes.push({
|
|
227
|
-
productId: product.id,
|
|
228
|
-
rating: 5
|
|
229
|
-
});
|
|
176
|
+
### Auto-Persist Proxy
|
|
230
177
|
|
|
231
|
-
|
|
232
|
-
const userLikes = user.likes().map(l => l.product()); // Products Alice likes
|
|
233
|
-
const productLikers = product.likes().map(l => l.user()); // Users who like laptop
|
|
178
|
+
Setting a property on an entity auto-updates the DB:
|
|
234
179
|
|
|
235
|
-
|
|
236
|
-
|
|
180
|
+
```typescript
|
|
181
|
+
const alice = db.users.select().where({ id: 1 }).get()!;
|
|
182
|
+
alice.score = 200; // → UPDATE users SET score = 200 WHERE id = 1
|
|
237
183
|
```
|
|
238
184
|
|
|
239
|
-
|
|
185
|
+
---
|
|
240
186
|
|
|
241
|
-
|
|
187
|
+
## Schema Validation
|
|
242
188
|
|
|
243
|
-
|
|
189
|
+
Zod validates every insert and update at runtime:
|
|
244
190
|
|
|
245
191
|
```typescript
|
|
246
|
-
|
|
247
|
-
id: 'custom-id-123', // Explicit ID
|
|
248
|
-
name: 'Custom Entity'
|
|
249
|
-
});
|
|
192
|
+
db.users.insert({ name: '', email: 'bad', age: -1 }); // throws ZodError
|
|
250
193
|
```
|
|
251
194
|
|
|
252
|
-
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Indexes
|
|
253
198
|
|
|
254
199
|
```typescript
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
200
|
+
const db = new Database(':memory:', schemas, {
|
|
201
|
+
indexes: {
|
|
202
|
+
users: ['email', ['name', 'role']],
|
|
203
|
+
books: ['author_id', 'year'],
|
|
204
|
+
},
|
|
260
205
|
});
|
|
261
206
|
```
|
|
262
207
|
|
|
263
|
-
|
|
208
|
+
---
|
|
264
209
|
|
|
265
|
-
|
|
210
|
+
## Change Tracking & Events
|
|
266
211
|
|
|
267
212
|
```typescript
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
email: z.string().email(),
|
|
271
|
-
age: z.number().optional(),
|
|
272
|
-
});
|
|
213
|
+
const db = new Database(':memory:', schemas, { changeTracking: true });
|
|
214
|
+
db.getChangesSince(0);
|
|
273
215
|
|
|
274
|
-
|
|
275
|
-
const user = db.users.insert({
|
|
276
|
-
name: 'A', // Too short!
|
|
277
|
-
email: 'invalid-email' // Invalid format!
|
|
278
|
-
});
|
|
216
|
+
db.users.subscribe('insert', (user) => console.log('New:', user.name));
|
|
279
217
|
```
|
|
280
218
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
1. **Use Explicit Junction Tables**: Define junction entities with additional fields rather than relying on auto-generated tables
|
|
284
|
-
2. **Leverage Reactive Editing**: Change properties directly instead of calling update methods
|
|
285
|
-
3. **Subscribe to Events**: Use subscriptions for real-time updates and side effects
|
|
286
|
-
4. **Validate with Zod**: Define comprehensive schemas with proper validation rules
|
|
287
|
-
5. **Use Transactions**: Wrap related operations in transactions for data consistency
|
|
219
|
+
---
|
|
288
220
|
|
|
289
|
-
##
|
|
221
|
+
## Smart Polling
|
|
290
222
|
|
|
291
223
|
```typescript
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// SatiDB
|
|
298
|
-
const user = db.users.get(id);
|
|
299
|
-
user.name = 'New Name'; // Automatically saved!
|
|
300
|
-
```
|
|
224
|
+
const unsub = db.users.select()
|
|
225
|
+
.where({ role: 'admin' })
|
|
226
|
+
.subscribe((admins) => {
|
|
227
|
+
console.log('Admin list changed:', admins);
|
|
228
|
+
}, { interval: 1000 });
|
|
301
229
|
|
|
302
|
-
|
|
230
|
+
unsub();
|
|
231
|
+
```
|
|
303
232
|
|
|
304
|
-
|
|
305
|
-
- TypeScript (recommended) or JavaScript
|
|
306
|
-
- Zod for schema validation
|
|
233
|
+
---
|
|
307
234
|
|
|
308
|
-
##
|
|
235
|
+
## Examples & Tests
|
|
309
236
|
|
|
310
|
-
|
|
237
|
+
```bash
|
|
238
|
+
bun examples/example.ts # comprehensive demo
|
|
239
|
+
bun test # 91 tests
|
|
240
|
+
```
|
|
311
241
|
|
|
312
242
|
---
|
|
313
243
|
|
|
314
|
-
|
|
244
|
+
## API Reference
|
|
245
|
+
|
|
246
|
+
| Method | Description |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `new Database(path, schemas, options?)` | Create database with Zod schemas |
|
|
249
|
+
| **Querying** | |
|
|
250
|
+
| `db.table.select(...cols?).where(filter).get()` | Single row |
|
|
251
|
+
| `db.table.select(...cols?).where(filter).all()` | Array of rows |
|
|
252
|
+
| `db.table.select().count()` | Count rows |
|
|
253
|
+
| `db.table.select().join(db.other, cols?).all()` | Fluent join (auto FK) |
|
|
254
|
+
| `db.query(c => { ... })` | Proxy callback (SQL-like JOINs) |
|
|
255
|
+
| **Writing** | |
|
|
256
|
+
| `db.table.insert(data)` | Insert with validation |
|
|
257
|
+
| `db.table.update(id, data)` | Update by ID |
|
|
258
|
+
| `db.table.update(data).where(filter).exec()` | Fluent update |
|
|
259
|
+
| `db.table.upsert(match, data)` | Insert or update |
|
|
260
|
+
| `db.table.delete(id)` | Delete by ID |
|
|
261
|
+
| **Navigation** | |
|
|
262
|
+
| `entity.navMethod()` | Lazy navigation (FK name minus `_id`) |
|
|
263
|
+
| `entity.update(data)` | Update entity in-place |
|
|
264
|
+
| `entity.delete()` | Delete entity |
|
|
265
|
+
| **Events** | |
|
|
266
|
+
| `db.table.subscribe(event, callback)` | Listen for insert/update/delete |
|
|
267
|
+
| `db.table.select().subscribe(cb, opts)` | Smart polling |
|
|
268
|
+
| `db.getChangesSince(version, table?)` | Change tracking |
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT
|