sqlite-zod-orm 3.5.2 → 3.6.1

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,7 +4714,13 @@ 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: (callback, options2) => this._createOnStream(entityName, callback, options2?.interval),
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
+ },
4718
4724
  _tableName: entityName
4719
4725
  };
4720
4726
  this[key] = accessor;
@@ -4793,6 +4799,61 @@ class _Database {
4793
4799
  stopped = true;
4794
4800
  };
4795
4801
  }
4802
+ _createChangeStream(entityName, eventType, callback, intervalOverride) {
4803
+ const interval = intervalOverride ?? this.pollInterval;
4804
+ const allRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all();
4805
+ let snapshot = new Map;
4806
+ let snapshotEntities = new Map;
4807
+ for (const row of allRows) {
4808
+ snapshot.set(row.id, JSON.stringify(row));
4809
+ snapshotEntities.set(row.id, row);
4810
+ }
4811
+ let lastRevision = this._getRevision(entityName);
4812
+ let stopped = false;
4813
+ const poll = async () => {
4814
+ if (stopped)
4815
+ return;
4816
+ const currentRevision = this._getRevision(entityName);
4817
+ if (currentRevision !== lastRevision) {
4818
+ lastRevision = currentRevision;
4819
+ const currentRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all();
4820
+ const currentMap = new Map;
4821
+ const currentEntities = new Map;
4822
+ for (const row of currentRows) {
4823
+ currentMap.set(row.id, JSON.stringify(row));
4824
+ currentEntities.set(row.id, row);
4825
+ }
4826
+ if (eventType === "update") {
4827
+ for (const [id, json] of currentMap) {
4828
+ if (stopped)
4829
+ return;
4830
+ if (snapshot.has(id) && snapshot.get(id) !== json) {
4831
+ const entity = this._attachMethods(entityName, transformFromStorage(currentEntities.get(id), this.schemas[entityName]));
4832
+ const oldEntity = this._attachMethods(entityName, transformFromStorage(snapshotEntities.get(id), this.schemas[entityName]));
4833
+ await callback(entity, oldEntity);
4834
+ }
4835
+ }
4836
+ } else if (eventType === "delete") {
4837
+ for (const [id] of snapshot) {
4838
+ if (stopped)
4839
+ return;
4840
+ if (!currentMap.has(id)) {
4841
+ const oldEntity = this._attachMethods(entityName, transformFromStorage(snapshotEntities.get(id), this.schemas[entityName]));
4842
+ await callback(oldEntity);
4843
+ }
4844
+ }
4845
+ }
4846
+ snapshot = currentMap;
4847
+ snapshotEntities = currentEntities;
4848
+ }
4849
+ if (!stopped)
4850
+ setTimeout(poll, interval);
4851
+ };
4852
+ setTimeout(poll, interval);
4853
+ return () => {
4854
+ stopped = true;
4855
+ };
4856
+ }
4796
4857
  insert(entityName, data) {
4797
4858
  const schema = this.schemas[entityName];
4798
4859
  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.5.2",
3
+ "version": "3.6.1",
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,8 +59,11 @@ 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: (callback: (row: any) => void | Promise<void>, options?: { interval?: number }) =>
63
- this._createOnStream(entityName, callback, options?.interval),
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
+ },
64
67
  _tableName: entityName,
65
68
  };
66
69
  (this as any)[key] = accessor;
@@ -201,6 +204,92 @@ class _Database<Schemas extends SchemaMap> {
201
204
  return () => { stopped = true; };
202
205
  }
203
206
 
207
+ /**
208
+ * Stream specific mutations (update or delete) with natural callback signatures.
209
+ *
210
+ * Maintains a full snapshot and diffs on each poll.
211
+ * Only fires for the subscribed event type.
212
+ *
213
+ * - 'update': callback(row, oldRow)
214
+ * - 'delete': callback(row)
215
+ */
216
+ public _createChangeStream(
217
+ entityName: string,
218
+ eventType: 'update' | 'delete',
219
+ callback: (...args: any[]) => void | Promise<void>,
220
+ intervalOverride?: number,
221
+ ): () => void {
222
+ const interval = intervalOverride ?? this.pollInterval;
223
+
224
+ // Build initial snapshot: Map<id, serialized row>
225
+ const allRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all() as any[];
226
+ let snapshot = new Map<number, string>();
227
+ let snapshotEntities = new Map<number, any>();
228
+ for (const row of allRows) {
229
+ snapshot.set(row.id, JSON.stringify(row));
230
+ snapshotEntities.set(row.id, row);
231
+ }
232
+
233
+ let lastRevision: string = this._getRevision(entityName);
234
+ let stopped = false;
235
+
236
+ const poll = async () => {
237
+ if (stopped) return;
238
+
239
+ const currentRevision = this._getRevision(entityName);
240
+ if (currentRevision !== lastRevision) {
241
+ lastRevision = currentRevision;
242
+
243
+ const currentRows = this.db.query(`SELECT * FROM "${entityName}" ORDER BY id ASC`).all() as any[];
244
+ const currentMap = new Map<number, string>();
245
+ const currentEntities = new Map<number, any>();
246
+ for (const row of currentRows) {
247
+ currentMap.set(row.id, JSON.stringify(row));
248
+ currentEntities.set(row.id, row);
249
+ }
250
+
251
+ if (eventType === 'update') {
252
+ // Detect updates: existing rows whose JSON changed
253
+ for (const [id, json] of currentMap) {
254
+ if (stopped) return;
255
+ if (snapshot.has(id) && snapshot.get(id) !== json) {
256
+ const entity = this._attachMethods(
257
+ entityName,
258
+ transformFromStorage(currentEntities.get(id), this.schemas[entityName]!)
259
+ );
260
+ const oldEntity = this._attachMethods(
261
+ entityName,
262
+ transformFromStorage(snapshotEntities.get(id), this.schemas[entityName]!)
263
+ );
264
+ await callback(entity, oldEntity);
265
+ }
266
+ }
267
+ } else if (eventType === 'delete') {
268
+ // Detect deletes: rows in snapshot but not in current
269
+ for (const [id] of snapshot) {
270
+ if (stopped) return;
271
+ if (!currentMap.has(id)) {
272
+ const oldEntity = this._attachMethods(
273
+ entityName,
274
+ transformFromStorage(snapshotEntities.get(id), this.schemas[entityName]!)
275
+ );
276
+ await callback(oldEntity);
277
+ }
278
+ }
279
+ }
280
+
281
+ snapshot = currentMap;
282
+ snapshotEntities = currentEntities;
283
+ }
284
+
285
+ if (!stopped) setTimeout(poll, interval);
286
+ };
287
+
288
+ setTimeout(poll, interval);
289
+
290
+ return () => { stopped = true; };
291
+ }
292
+
204
293
  // ===========================================================================
205
294
  // CRUD
206
295
  // ===========================================================================
package/src/types.ts CHANGED
@@ -45,6 +45,7 @@ export type Relationship = {
45
45
  foreignKey: string;
46
46
  };
47
47
 
48
+
48
49
  // =============================================================================
49
50
  // Type Helpers
50
51
  // =============================================================================
@@ -149,12 +150,20 @@ export type NavEntityAccessor<
149
150
  delete: (id: number) => void;
150
151
  select: (...cols: (keyof z.infer<S[Table & keyof S]> & string)[]) => QueryBuilder<NavEntity<S, R, Table>>;
151
152
  /**
152
- * Stream new rows one at a time, in insertion order.
153
- * Only emits rows inserted AFTER subscription starts.
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
+ *
154
159
  * Callbacks are awaited — strict ordering guaranteed even with async handlers.
155
160
  * @returns Unsubscribe function.
156
161
  */
157
- on: (callback: (row: NavEntity<S, R, Table>) => void | Promise<void>, options?: { interval?: number }) => () => void;
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
+ };
158
167
  _tableName: string;
159
168
  readonly _schema?: S[Table & keyof S];
160
169
  };
@@ -181,12 +190,20 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
181
190
  delete: (id: number) => void;
182
191
  select: (...cols: (keyof InferSchema<S> & string)[]) => QueryBuilder<AugmentedEntity<S>>;
183
192
  /**
184
- * Stream new rows one at a time, in insertion order.
185
- * Only emits rows inserted AFTER subscription starts.
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
+ *
186
199
  * Callbacks are awaited — strict ordering guaranteed even with async handlers.
187
200
  * @returns Unsubscribe function.
188
201
  */
189
- on: (callback: (row: AugmentedEntity<S>) => void | Promise<void>, options?: { interval?: number }) => () => void;
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
+ };
190
207
  _tableName: string;
191
208
  readonly _schema?: S;
192
209
  };