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 +35 -5
- package/dist/index.js +20 -0
- package/package.json +1 -1
- package/src/database.ts +49 -0
- package/src/types.ts +12 -0
package/README.md
CHANGED
|
@@ -207,9 +207,37 @@ const db = new Database(':memory:', schemas, {
|
|
|
207
207
|
|
|
208
208
|
---
|
|
209
209
|
|
|
210
|
-
## Reactivity
|
|
210
|
+
## Reactivity
|
|
211
211
|
|
|
212
|
-
|
|
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/
|
|
295
|
-
bun
|
|
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
|
-
| `
|
|
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
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
|
};
|