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 CHANGED
@@ -1,314 +1,272 @@
1
- # SatiDB 🗄️
1
+ # sqlite-zod-orm
2
2
 
3
- A modern, type-safe SQLite wrapper with **reactive editing**, **automatic relationships**, and **fluent APIs**. Built for TypeScript/JavaScript with Zod schema validation.
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
- ## ✨ Key Features
6
-
7
- - **🔄 Reactive Editing**: Change properties directly, auto-persists to database
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
- ## 🚀 Quick Start
9
+ ## Quick Start
16
10
 
17
11
  ```typescript
18
- import { MyDatabase, z } from './satidb';
19
-
20
- // Define schemas with relationships
21
- const AuthorSchema = z.object({
22
- name: z.string(),
23
- posts: z.lazy(() => z.array(PostSchema)).optional(),
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
- // Use it!
39
- const author = db.authors.insert({ name: 'Jane Doe' });
40
- const post = author.posts.push({ title: 'Hello World', content: '...' });
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
- ## 📖 Core Concepts
27
+ ---
47
28
 
48
- ### Schemas & Relationships
29
+ ## Defining Relationships
49
30
 
50
- Define entities using Zod schemas with lazy relationships:
31
+ FK columns go in your schema. The `relations` config declares which FK points to which table:
51
32
 
52
33
  ```typescript
53
- // One-to-many: Author has many Posts
54
- const AuthorSchema = z.object({
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
- // Belongs-to: Post belongs to Author
60
- const PostSchema = z.object({
61
- title: z.string(),
62
- author: z.lazy(() => AuthorSchema).optional(), // belongs-to
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
- ### Explicit Junction Tables
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
- For many-to-many relationships, define explicit junction entities with additional fields:
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
- ```typescript
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
- **Why explicit junction tables?**
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
- ## 🎯 API Reference
58
+ ## Querying `select()` is the only path
95
59
 
96
- ### Creating & Querying
60
+ All queries go through `select()`:
97
61
 
98
62
  ```typescript
99
- // Insert new entities
100
- const user = db.users.insert({ name: 'Alice', email: 'alice@example.com' });
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
- // Update
110
- const updated = db.users.update('user-id', { name: 'Alice Smith' });
66
+ // All matching rows
67
+ const admins = db.users.select().where({ role: 'admin' }).all();
111
68
 
112
- // Upsert
113
- const user = db.users.upsert({ email: 'alice@example.com', name: 'Alice' });
69
+ // All rows
70
+ const everyone = db.users.select().all();
114
71
 
115
- // Delete
116
- db.users.delete('user-id');
72
+ // Count
73
+ const count = db.users.select().count();
117
74
  ```
118
75
 
119
- ### Relationships
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
- // Belongs-to: Navigate relationships
130
- const postAuthor = post.author();
78
+ `$gt` `$gte` `$lt` `$lte` `$ne` `$in`
131
79
 
132
- // Get specific related entity
133
- const specificPost = author.post('post-id-123');
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
- ### Reactive Editing
88
+ ### `$or`
137
89
 
138
90
  ```typescript
139
- const user = db.users.get('user-id');
91
+ const results = db.users.select()
92
+ .where({ $or: [{ role: 'admin' }, { score: { $gt: 50 } }] })
93
+ .all();
94
+ ```
95
+
96
+ ### Fluent Join
140
97
 
141
- // Just change properties - automatically persists!
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
- // No need to call .save() or .update()
147
- // Changes are immediately persisted to database
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
- ### Event Subscriptions
109
+ ### `db.query()` — Proxy Query (SQL-like)
110
+
111
+ Full SQL-like control with destructured table aliases:
151
112
 
152
113
  ```typescript
153
- // Listen to database events
154
- db.users.subscribe('insert', (user) => {
155
- console.log(`New user: ${user.name}`);
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
- db.posts.subscribe('update', (post) => {
159
- console.log(`Post updated: ${post.title}`);
160
- });
125
+ ---
161
126
 
162
- db.users.subscribe('delete', (user) => {
163
- console.log(`User deleted: ${user.name}`);
164
- });
165
- ```
127
+ ## Lazy Navigation
166
128
 
167
- ### Transactions
129
+ Relationship fields become callable methods on entities. The method name is the FK column with `_id` stripped:
168
130
 
169
131
  ```typescript
170
- const result = db.transaction(() => {
171
- const author = db.authors.insert({ name: 'Jane' });
172
- const post1 = author.posts.push({ title: 'Post 1' });
173
- const post2 = author.posts.push({ title: 'Post 2' });
174
- return { author, posts: [post1, post2] };
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
- ## 🔗 Relationship Types
143
+ ---
179
144
 
180
- ### One-to-Many
145
+ ## CRUD
181
146
 
182
147
  ```typescript
183
- const PersonalitySchema = z.object({
184
- name: z.string(),
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
- const ChatSchema = z.object({
189
- title: z.string(),
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
- // Usage
194
- const personality = db.personalities.insert({ name: 'Assistant' });
195
- const chat = personality.chats.push({ title: 'Help Session' });
196
- const chatPersonality = chat.personality(); // Navigate back
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
- ### Many-to-Many (Explicit Junction)
160
+ // Entity-level update
161
+ user.update({ role: 'superadmin' });
200
162
 
201
- ```typescript
202
- // Define all three entities
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
- const ProductSchema = z.object({
209
- name: z.string(),
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
- // Junction table with additional fields
214
- const LikeSchema = z.object({
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
- // Usage
222
- const user = db.users.insert({ name: 'Alice' });
223
- const product = db.products.insert({ name: 'Laptop' });
172
+ // Delete
173
+ db.users.delete(1);
174
+ ```
224
175
 
225
- // Create relationship with metadata
226
- const like = user.likes.push({
227
- productId: product.id,
228
- rating: 5
229
- });
176
+ ### Auto-Persist Proxy
230
177
 
231
- // Query from both sides
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
- // Junction table is a full entity
236
- like.rating = 4; // Reactive update on junction table!
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
- ## 📝 Advanced Usage
185
+ ---
240
186
 
241
- ### Custom ID Generation
187
+ ## Schema Validation
242
188
 
243
- IDs are automatically generated based on entity data hash. For custom IDs:
189
+ Zod validates every insert and update at runtime:
244
190
 
245
191
  ```typescript
246
- const entity = db.entities.insert({
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
- ### Query Options
195
+ ---
196
+
197
+ ## Indexes
253
198
 
254
199
  ```typescript
255
- const results = db.posts.find({
256
- published: true,
257
- $limit: 10,
258
- $offset: 20,
259
- $sortBy: 'createdAt:desc'
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
- ### Schema Validation
208
+ ---
264
209
 
265
- All data is validated against Zod schemas:
210
+ ## Change Tracking & Events
266
211
 
267
212
  ```typescript
268
- const UserSchema = z.object({
269
- name: z.string().min(2),
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
- // Throws validation error if invalid
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
- ## 🎯 Best Practices
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
- ## 🔄 Migration from Traditional ORMs
221
+ ## Smart Polling
290
222
 
291
223
  ```typescript
292
- // Traditional ORM
293
- const user = await User.findById(id);
294
- user.name = 'New Name';
295
- await user.save(); // Explicit save
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
- ## 🛠️ Requirements
230
+ unsub();
231
+ ```
303
232
 
304
- - Bun runtime with SQLite support
305
- - TypeScript (recommended) or JavaScript
306
- - Zod for schema validation
233
+ ---
307
234
 
308
- ## 📄 License
235
+ ## Examples & Tests
309
236
 
310
- MIT License - feel free to use in your projects!
237
+ ```bash
238
+ bun examples/example.ts # comprehensive demo
239
+ bun test # 91 tests
240
+ ```
311
241
 
312
242
  ---
313
243
 
314
- **SatiDB** - Reactive, type-safe database operations made simple. 🚀
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