sqlite-zod-orm 3.3.1 → 3.4.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
@@ -207,9 +207,37 @@ const db = new Database(':memory:', schemas, {
207
207
 
208
208
  ---
209
209
 
210
- ## Reactivity — `select().subscribe()`
210
+ ## Reactivity
211
211
 
212
- One API to watch any query for changes. Detects **all** mutations (inserts, updates, deletes) with zero disk overhead.
212
+ Two complementary APIs for watching data changes:
213
+
214
+ | API | Receives | Fires on | Use case |
215
+ |---|---|---|---|
216
+ | **`db.table.on(cb)`** | One row at a time, in order | New inserts only | Message streams, event queues |
217
+ | **`select().subscribe(cb)`** | Full result snapshot | Any change (insert/update/delete) | Live dashboards, filtered views |
218
+
219
+ ---
220
+
221
+ ### Row Stream — `db.table.on(callback)`
222
+
223
+ Streams new rows one at a time, in insertion order. Only emits rows created **after** subscription starts.
224
+
225
+ ```typescript
226
+ const unsub = db.messages.on((msg) => {
227
+ console.log(`${msg.author}: ${msg.text}`);
228
+ }, { interval: 200 });
229
+
230
+ // Later: stop listening
231
+ unsub();
232
+ ```
233
+
234
+ If 5 messages arrive between polls, the callback fires 5 times — once per row, in order. Uses a watermark (`id > lastSeen`) internally.
235
+
236
+ ---
237
+
238
+ ### Snapshot — `select().subscribe(callback)`
239
+
240
+ Returns the **full query result** whenever data changes. Detects all mutations (inserts, updates, deletes).
213
241
 
214
242
  ```typescript
215
243
  const unsub = db.users.select()
@@ -291,8 +319,9 @@ Run `bun examples/messages-demo.ts` for a full working demo.
291
319
  ## Examples & Tests
292
320
 
293
321
  ```bash
294
- bun examples/example.ts # comprehensive demo
295
- bun test # 91 tests
322
+ bun examples/messages-demo.ts # .on() vs .subscribe() demo
323
+ bun examples/example.ts # comprehensive demo
324
+ bun test # 93 tests
296
325
  ```
297
326
 
298
327
  ---
@@ -319,7 +348,8 @@ bun test # 91 tests
319
348
  | `entity.update(data)` | Update entity in-place |
320
349
  | `entity.delete()` | Delete entity |
321
350
  | **Reactivity** | |
322
- | `select().subscribe(cb, opts?)` | Watch any query for changes (all mutations) |
351
+ | `db.table.on(cb, opts?)` | Stream new rows one at a time, in order |
352
+ | `select().subscribe(cb, opts?)` | Watch query result snapshot (all mutations) |
323
353
 
324
354
  ## License
325
355
 
package/dist/index.js CHANGED
@@ -4680,6 +4680,7 @@ class _Database {
4680
4680
  upsert: (conditions, data) => this.upsert(entityName, data, conditions),
4681
4681
  delete: (id) => this.delete(entityName, id),
4682
4682
  select: (...cols) => this._createQueryBuilder(entityName, cols),
4683
+ on: (callback, options2) => this._createOnStream(entityName, callback, options2),
4683
4684
  _tableName: entityName
4684
4685
  };
4685
4686
  this[key] = accessor;
@@ -4729,6 +4730,25 @@ class _Database {
4729
4730
  const dataVersion = this.db.query("PRAGMA data_version").get()?.data_version ?? 0;
4730
4731
  return `${rev}:${dataVersion}`;
4731
4732
  }
4733
+ _createOnStream(entityName, callback, options) {
4734
+ const { interval = 500 } = options ?? {};
4735
+ const maxRow = this.db.query(`SELECT MAX(id) as _max FROM "${entityName}"`).get();
4736
+ let lastMaxId = maxRow?._max ?? 0;
4737
+ let lastRevision = this._getRevision(entityName);
4738
+ const timer = setInterval(() => {
4739
+ const currentRevision = this._getRevision(entityName);
4740
+ if (currentRevision === lastRevision)
4741
+ return;
4742
+ lastRevision = currentRevision;
4743
+ const newRows = this.db.query(`SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`).all(lastMaxId);
4744
+ for (const rawRow of newRows) {
4745
+ const entity = this._attachMethods(entityName, transformFromStorage(rawRow, this.schemas[entityName]));
4746
+ callback(entity);
4747
+ lastMaxId = rawRow.id;
4748
+ }
4749
+ }, interval);
4750
+ return () => clearInterval(timer);
4751
+ }
4732
4752
  insert(entityName, data) {
4733
4753
  const schema = this.schemas[entityName];
4734
4754
  const validatedData = asZodObject(schema).passthrough().parse(data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-zod-orm",
3
- "version": "3.3.1",
3
+ "version": "3.4.0",
4
4
  "description": "Type-safe SQLite ORM for Bun — Zod schemas, fluent queries, auto relationships, zero SQL",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/database.ts CHANGED
@@ -57,6 +57,8 @@ class _Database<Schemas extends SchemaMap> {
57
57
  upsert: (conditions, data) => this.upsert(entityName, data, conditions),
58
58
  delete: (id) => this.delete(entityName, id),
59
59
  select: (...cols: string[]) => this._createQueryBuilder(entityName, cols),
60
+ on: (callback: (row: any) => void, options?: { interval?: number }) =>
61
+ this._createOnStream(entityName, callback, options),
60
62
  _tableName: entityName,
61
63
  };
62
64
  (this as any)[key] = accessor;
@@ -137,6 +139,53 @@ class _Database<Schemas extends SchemaMap> {
137
139
  return `${rev}:${dataVersion}`;
138
140
  }
139
141
 
142
+ // ===========================================================================
143
+ // Row Stream — .on(callback)
144
+ // ===========================================================================
145
+
146
+ /**
147
+ * Stream new rows one at a time, in insertion order.
148
+ *
149
+ * Uses a watermark (last seen id) to query only `WHERE id > ?`.
150
+ * Checks revision + data_version first to avoid unnecessary queries.
151
+ */
152
+ public _createOnStream(
153
+ entityName: string,
154
+ callback: (row: any) => void,
155
+ options?: { interval?: number },
156
+ ): () => void {
157
+ const { interval = 500 } = options ?? {};
158
+
159
+ // Initialize watermark to current max id (only emit NEW rows)
160
+ const maxRow = this.db.query(`SELECT MAX(id) as _max FROM "${entityName}"`).get() as any;
161
+ let lastMaxId: number = maxRow?._max ?? 0;
162
+ let lastRevision: string = this._getRevision(entityName);
163
+
164
+ const timer = setInterval(() => {
165
+ // Fast check: did anything change?
166
+ const currentRevision = this._getRevision(entityName);
167
+ if (currentRevision === lastRevision) return;
168
+ lastRevision = currentRevision;
169
+
170
+ // Fetch new rows since watermark
171
+ const newRows = this.db.query(
172
+ `SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`
173
+ ).all(lastMaxId) as any[];
174
+
175
+ for (const rawRow of newRows) {
176
+ // Hydrate with entity methods and schema transforms
177
+ const entity = this._attachMethods(
178
+ entityName,
179
+ transformFromStorage(rawRow, this.schemas[entityName]!)
180
+ );
181
+ callback(entity);
182
+ lastMaxId = rawRow.id;
183
+ }
184
+ }, interval);
185
+
186
+ return () => clearInterval(timer);
187
+ }
188
+
140
189
  // ===========================================================================
141
190
  // CRUD
142
191
  // ===========================================================================
package/src/types.ts CHANGED
@@ -143,6 +143,12 @@ export type NavEntityAccessor<
143
143
  upsert: (conditions?: Partial<z.infer<S[Table & keyof S]>>, data?: Partial<z.infer<S[Table & keyof S]>>) => NavEntity<S, R, Table>;
144
144
  delete: (id: number) => void;
145
145
  select: (...cols: (keyof z.infer<S[Table & keyof S]> & string)[]) => QueryBuilder<NavEntity<S, R, Table>>;
146
+ /**
147
+ * Stream new rows one at a time, in insertion order.
148
+ * Only emits rows inserted AFTER subscription starts.
149
+ * @returns Unsubscribe function.
150
+ */
151
+ on: (callback: (row: NavEntity<S, R, Table>) => void, options?: { interval?: number }) => () => void;
146
152
  _tableName: string;
147
153
  readonly _schema?: S[Table & keyof S];
148
154
  };
@@ -168,6 +174,12 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
168
174
  upsert: (conditions?: Partial<InferSchema<S>>, data?: Partial<InferSchema<S>>) => AugmentedEntity<S>;
169
175
  delete: (id: number) => void;
170
176
  select: (...cols: (keyof InferSchema<S> & string)[]) => QueryBuilder<AugmentedEntity<S>>;
177
+ /**
178
+ * Stream new rows one at a time, in insertion order.
179
+ * Only emits rows inserted AFTER subscription starts.
180
+ * @returns Unsubscribe function.
181
+ */
182
+ on: (callback: (row: AugmentedEntity<S>) => void, options?: { interval?: number }) => () => void;
171
183
  _tableName: string;
172
184
  readonly _schema?: S;
173
185
  };