sqlite-zod-orm 3.6.2 → 3.7.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/dist/index.js CHANGED
@@ -4714,13 +4714,6 @@ class _Database {
4714
4714
  upsert: (conditions, data) => this.upsert(entityName, data, conditions),
4715
4715
  delete: (id) => this.delete(entityName, id),
4716
4716
  select: (...cols) => this._createQueryBuilder(entityName, cols),
4717
- on: (event, callback, options2) => {
4718
- if (event === "insert")
4719
- return this._createOnStream(entityName, callback, options2?.interval);
4720
- if (event === "update" || event === "delete")
4721
- return this._createChangeStream(entityName, event, callback, options2?.interval);
4722
- throw new Error(`Unknown event type: '${event}'. Supported: 'insert', 'update', 'delete'`);
4723
- },
4724
4717
  _tableName: entityName
4725
4718
  };
4726
4719
  this[key] = accessor;
@@ -4770,122 +4763,6 @@ class _Database {
4770
4763
  const dataVersion = this.db.query("PRAGMA data_version").get()?.data_version ?? 0;
4771
4764
  return `${rev}:${dataVersion}`;
4772
4765
  }
4773
- _createOnStream(entityName, callback, intervalOverride) {
4774
- const interval = intervalOverride ?? this.pollInterval;
4775
- const maxRow = this.db.query(`SELECT MAX(id) as _max FROM "${entityName}"`).get();
4776
- let lastMaxId = maxRow?._max ?? 0;
4777
- let lastRevision = this._getRevision(entityName);
4778
- let stopped = false;
4779
- const poll = async () => {
4780
- if (stopped)
4781
- return;
4782
- const currentRevision = this._getRevision(entityName);
4783
- if (currentRevision !== lastRevision) {
4784
- lastRevision = currentRevision;
4785
- const newRows = this.db.query(`SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`).all(lastMaxId);
4786
- for (const rawRow of newRows) {
4787
- if (stopped)
4788
- return;
4789
- const entity = this._attachMethods(entityName, transformFromStorage(rawRow, this.schemas[entityName]));
4790
- await callback(entity);
4791
- lastMaxId = rawRow.id;
4792
- }
4793
- }
4794
- if (!stopped)
4795
- setTimeout(poll, interval);
4796
- };
4797
- setTimeout(poll, interval);
4798
- return () => {
4799
- stopped = true;
4800
- };
4801
- }
4802
- _watchers = new Map;
4803
- _getWatcher(entityName) {
4804
- if (this._watchers.has(entityName))
4805
- return this._watchers.get(entityName);
4806
- const allRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all();
4807
- const snapshot = new Map;
4808
- const snapshotEntities = new Map;
4809
- for (const row of allRows) {
4810
- snapshot.set(row.id, JSON.stringify(row));
4811
- snapshotEntities.set(row.id, row);
4812
- }
4813
- const watcher = {
4814
- updateListeners: new Set,
4815
- deleteListeners: new Set,
4816
- snapshot,
4817
- snapshotEntities,
4818
- lastRevision: this._getRevision(entityName),
4819
- stopped: false
4820
- };
4821
- this._watchers.set(entityName, watcher);
4822
- const interval = this.pollInterval;
4823
- const poll = async () => {
4824
- if (watcher.stopped)
4825
- return;
4826
- const currentRevision = this._getRevision(entityName);
4827
- if (currentRevision !== watcher.lastRevision) {
4828
- watcher.lastRevision = currentRevision;
4829
- const currentRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all();
4830
- const currentMap = new Map;
4831
- const currentEntities = new Map;
4832
- for (const row of currentRows) {
4833
- currentMap.set(row.id, JSON.stringify(row));
4834
- currentEntities.set(row.id, row);
4835
- }
4836
- if (watcher.updateListeners.size > 0) {
4837
- for (const [id, json] of currentMap) {
4838
- if (watcher.stopped)
4839
- return;
4840
- if (watcher.snapshot.has(id) && watcher.snapshot.get(id) !== json) {
4841
- const entity = this._attachMethods(entityName, transformFromStorage(currentEntities.get(id), this.schemas[entityName]));
4842
- const oldEntity = this._attachMethods(entityName, transformFromStorage(watcher.snapshotEntities.get(id), this.schemas[entityName]));
4843
- for (const cb of watcher.updateListeners) {
4844
- await cb(entity, oldEntity);
4845
- }
4846
- }
4847
- }
4848
- }
4849
- if (watcher.deleteListeners.size > 0) {
4850
- for (const [id] of watcher.snapshot) {
4851
- if (watcher.stopped)
4852
- return;
4853
- if (!currentMap.has(id)) {
4854
- const oldEntity = this._attachMethods(entityName, transformFromStorage(watcher.snapshotEntities.get(id), this.schemas[entityName]));
4855
- for (const cb of watcher.deleteListeners) {
4856
- await cb(oldEntity);
4857
- }
4858
- }
4859
- }
4860
- }
4861
- watcher.snapshot = currentMap;
4862
- watcher.snapshotEntities = currentEntities;
4863
- }
4864
- if (!watcher.stopped)
4865
- setTimeout(poll, interval);
4866
- };
4867
- setTimeout(poll, interval);
4868
- return watcher;
4869
- }
4870
- _createChangeStream(entityName, eventType, callback, _intervalOverride) {
4871
- const watcher = this._getWatcher(entityName);
4872
- if (eventType === "update") {
4873
- watcher.updateListeners.add(callback);
4874
- } else {
4875
- watcher.deleteListeners.add(callback);
4876
- }
4877
- return () => {
4878
- if (eventType === "update") {
4879
- watcher.updateListeners.delete(callback);
4880
- } else {
4881
- watcher.deleteListeners.delete(callback);
4882
- }
4883
- if (watcher.updateListeners.size === 0 && watcher.deleteListeners.size === 0) {
4884
- watcher.stopped = true;
4885
- this._watchers.delete(entityName);
4886
- }
4887
- };
4888
- }
4889
4766
  insert(entityName, data) {
4890
4767
  const schema = this.schemas[entityName];
4891
4768
  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.6.2",
3
+ "version": "3.7.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
@@ -59,11 +59,6 @@ class _Database<Schemas extends SchemaMap> {
59
59
  upsert: (conditions, data) => this.upsert(entityName, data, conditions),
60
60
  delete: (id) => this.delete(entityName, id),
61
61
  select: (...cols: string[]) => this._createQueryBuilder(entityName, cols),
62
- on: (event: string, callback: (...args: any[]) => void | Promise<void>, options?: { interval?: number }) => {
63
- if (event === 'insert') return this._createOnStream(entityName, callback, options?.interval);
64
- if (event === 'update' || event === 'delete') return this._createChangeStream(entityName, event, callback, options?.interval);
65
- throw new Error(`Unknown event type: '${event}'. Supported: 'insert', 'update', 'delete'`);
66
- },
67
62
  _tableName: entityName,
68
63
  };
69
64
  (this as any)[key] = accessor;
@@ -144,202 +139,6 @@ class _Database<Schemas extends SchemaMap> {
144
139
  return `${rev}:${dataVersion}`;
145
140
  }
146
141
 
147
- // ===========================================================================
148
- // Row Stream — .on(callback)
149
- // ===========================================================================
150
-
151
- /**
152
- * Stream new rows one at a time, in insertion order.
153
- *
154
- * Uses a watermark (last seen id) to query only `WHERE id > ?`.
155
- * Checks revision + data_version first to avoid unnecessary queries.
156
- */
157
- public _createOnStream(
158
- entityName: string,
159
- callback: (row: any) => void | Promise<void>,
160
- intervalOverride?: number,
161
- ): () => void {
162
- const interval = intervalOverride ?? this.pollInterval;
163
-
164
- // Initialize watermark to current max id (only emit NEW rows)
165
- const maxRow = this.db.query(`SELECT MAX(id) as _max FROM "${entityName}"`).get() as any;
166
- let lastMaxId: number = maxRow?._max ?? 0;
167
- let lastRevision: string = this._getRevision(entityName);
168
- let stopped = false;
169
-
170
- // Self-scheduling async loop: guarantees strict ordering
171
- // - Each callback (sync or async) completes before the next row is emitted
172
- // - Next poll only starts after the current batch is fully processed
173
- const poll = async () => {
174
- if (stopped) return;
175
-
176
- // Fast check: did anything change?
177
- const currentRevision = this._getRevision(entityName);
178
- if (currentRevision !== lastRevision) {
179
- lastRevision = currentRevision;
180
-
181
- // Fetch new rows since watermark
182
- const newRows = this.db.query(
183
- `SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`
184
- ).all(lastMaxId) as any[];
185
-
186
- for (const rawRow of newRows) {
187
- if (stopped) return; // bail if unsubscribed mid-batch
188
- const entity = this._attachMethods(
189
- entityName,
190
- transformFromStorage(rawRow, this.schemas[entityName]!)
191
- );
192
- await callback(entity); // await async callbacks
193
- lastMaxId = rawRow.id;
194
- }
195
- }
196
-
197
- // Schedule next poll only after this one is done
198
- if (!stopped) setTimeout(poll, interval);
199
- };
200
-
201
- // Start the loop
202
- setTimeout(poll, interval);
203
-
204
- return () => { stopped = true; };
205
- }
206
-
207
- // ===========================================================================
208
- // Table Watcher — shared snapshot-diff engine for update/delete events
209
- // ===========================================================================
210
-
211
- /** One watcher per table — shared by all update/delete listeners. */
212
- private _watchers = new Map<string, {
213
- updateListeners: Set<(row: any, oldRow: any) => void | Promise<void>>;
214
- deleteListeners: Set<(row: any) => void | Promise<void>>;
215
- snapshot: Map<number, string>;
216
- snapshotEntities: Map<number, any>;
217
- lastRevision: string;
218
- stopped: boolean;
219
- }>();
220
-
221
- /**
222
- * Get or create a shared table watcher. One SELECT * per change event,
223
- * no matter how many update/delete listeners are registered.
224
- */
225
- private _getWatcher(entityName: string) {
226
- if (this._watchers.has(entityName)) return this._watchers.get(entityName)!;
227
-
228
- // Build initial snapshot
229
- const allRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all() as any[];
230
- const snapshot = new Map<number, string>();
231
- const snapshotEntities = new Map<number, any>();
232
- for (const row of allRows) {
233
- snapshot.set(row.id, JSON.stringify(row));
234
- snapshotEntities.set(row.id, row);
235
- }
236
-
237
- const watcher = {
238
- updateListeners: new Set<(row: any, oldRow: any) => void | Promise<void>>(),
239
- deleteListeners: new Set<(row: any) => void | Promise<void>>(),
240
- snapshot,
241
- snapshotEntities,
242
- lastRevision: this._getRevision(entityName),
243
- stopped: false,
244
- };
245
- this._watchers.set(entityName, watcher);
246
-
247
- // Single poll loop for all listeners on this table
248
- const interval = this.pollInterval;
249
- const poll = async () => {
250
- if (watcher.stopped) return;
251
-
252
- const currentRevision = this._getRevision(entityName);
253
- if (currentRevision !== watcher.lastRevision) {
254
- watcher.lastRevision = currentRevision;
255
-
256
- const currentRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all() as any[];
257
- const currentMap = new Map<number, string>();
258
- const currentEntities = new Map<number, any>();
259
- for (const row of currentRows) {
260
- currentMap.set(row.id, JSON.stringify(row));
261
- currentEntities.set(row.id, row);
262
- }
263
-
264
- // Dispatch updates
265
- if (watcher.updateListeners.size > 0) {
266
- for (const [id, json] of currentMap) {
267
- if (watcher.stopped) return;
268
- if (watcher.snapshot.has(id) && watcher.snapshot.get(id) !== json) {
269
- const entity = this._attachMethods(
270
- entityName,
271
- transformFromStorage(currentEntities.get(id), this.schemas[entityName]!)
272
- );
273
- const oldEntity = this._attachMethods(
274
- entityName,
275
- transformFromStorage(watcher.snapshotEntities.get(id), this.schemas[entityName]!)
276
- );
277
- for (const cb of watcher.updateListeners) {
278
- await cb(entity, oldEntity);
279
- }
280
- }
281
- }
282
- }
283
-
284
- // Dispatch deletes
285
- if (watcher.deleteListeners.size > 0) {
286
- for (const [id] of watcher.snapshot) {
287
- if (watcher.stopped) return;
288
- if (!currentMap.has(id)) {
289
- const oldEntity = this._attachMethods(
290
- entityName,
291
- transformFromStorage(watcher.snapshotEntities.get(id), this.schemas[entityName]!)
292
- );
293
- for (const cb of watcher.deleteListeners) {
294
- await cb(oldEntity);
295
- }
296
- }
297
- }
298
- }
299
-
300
- watcher.snapshot = currentMap;
301
- watcher.snapshotEntities = currentEntities;
302
- }
303
-
304
- if (!watcher.stopped) setTimeout(poll, interval);
305
- };
306
-
307
- setTimeout(poll, interval);
308
- return watcher;
309
- }
310
-
311
- /**
312
- * Register an update or delete listener on the shared table watcher.
313
- * Returns an unsubscribe function that auto-stops the watcher when empty.
314
- */
315
- public _createChangeStream(
316
- entityName: string,
317
- eventType: 'update' | 'delete',
318
- callback: (...args: any[]) => void | Promise<void>,
319
- _intervalOverride?: number, // reserved, uses global pollInterval
320
- ): () => void {
321
- const watcher = this._getWatcher(entityName);
322
-
323
- if (eventType === 'update') {
324
- watcher.updateListeners.add(callback as any);
325
- } else {
326
- watcher.deleteListeners.add(callback as any);
327
- }
328
-
329
- return () => {
330
- if (eventType === 'update') {
331
- watcher.updateListeners.delete(callback as any);
332
- } else {
333
- watcher.deleteListeners.delete(callback as any);
334
- }
335
- // Auto-stop watcher when all listeners removed
336
- if (watcher.updateListeners.size === 0 && watcher.deleteListeners.size === 0) {
337
- watcher.stopped = true;
338
- this._watchers.delete(entityName);
339
- }
340
- };
341
- }
342
-
343
142
  // ===========================================================================
344
143
  // CRUD
345
144
  // ===========================================================================
package/src/types.ts CHANGED
@@ -149,21 +149,6 @@ export type NavEntityAccessor<
149
149
  upsert: (conditions?: Partial<z.infer<S[Table & keyof S]>>, data?: Partial<z.infer<S[Table & keyof S]>>) => NavEntity<S, R, Table>;
150
150
  delete: (id: number) => void;
151
151
  select: (...cols: (keyof z.infer<S[Table & keyof S]> & string)[]) => QueryBuilder<NavEntity<S, R, Table>>;
152
- /**
153
- * Listen for table events.
154
- *
155
- * `'insert'` — streams new rows, one at a time.
156
- * `'update'` — fires on row changes with (newRow, oldRow).
157
- * `'delete'` — fires when a row is removed.
158
- *
159
- * Callbacks are awaited — strict ordering guaranteed even with async handlers.
160
- * @returns Unsubscribe function.
161
- */
162
- on: {
163
- (event: 'insert', callback: (row: NavEntity<S, R, Table>) => void | Promise<void>, options?: { interval?: number }): () => void;
164
- (event: 'update', callback: (row: NavEntity<S, R, Table>, oldRow: NavEntity<S, R, Table>) => void | Promise<void>, options?: { interval?: number }): () => void;
165
- (event: 'delete', callback: (row: NavEntity<S, R, Table>) => void | Promise<void>, options?: { interval?: number }): () => void;
166
- };
167
152
  _tableName: string;
168
153
  readonly _schema?: S[Table & keyof S];
169
154
  };
@@ -189,21 +174,6 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
189
174
  upsert: (conditions?: Partial<InferSchema<S>>, data?: Partial<InferSchema<S>>) => AugmentedEntity<S>;
190
175
  delete: (id: number) => void;
191
176
  select: (...cols: (keyof InferSchema<S> & string)[]) => QueryBuilder<AugmentedEntity<S>>;
192
- /**
193
- * Listen for table events.
194
- *
195
- * `'insert'` — streams new rows, one at a time.
196
- * `'update'` — fires on row changes with (newRow, oldRow).
197
- * `'delete'` — fires when a row is removed.
198
- *
199
- * Callbacks are awaited — strict ordering guaranteed even with async handlers.
200
- * @returns Unsubscribe function.
201
- */
202
- on: {
203
- (event: 'insert', callback: (row: AugmentedEntity<S>) => void | Promise<void>, options?: { interval?: number }): () => void;
204
- (event: 'update', callback: (row: AugmentedEntity<S>, oldRow: AugmentedEntity<S>) => void | Promise<void>, options?: { interval?: number }): () => void;
205
- (event: 'delete', callback: (row: AugmentedEntity<S>) => void | Promise<void>, options?: { interval?: number }): () => void;
206
- };
207
177
  _tableName: string;
208
178
  readonly _schema?: S;
209
179
  };