sqlite-zod-orm 3.5.0 → 3.5.2
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 +1 -0
- package/dist/index.js +31 -17
- package/package.json +1 -1
- package/src/database.ts +40 -25
- package/src/query-builder.ts +4 -1
- package/src/types.ts +9 -2
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -177,7 +177,8 @@ class QueryBuilder {
|
|
|
177
177
|
conditionResolver;
|
|
178
178
|
revisionGetter;
|
|
179
179
|
eagerLoader;
|
|
180
|
-
|
|
180
|
+
defaultPollInterval;
|
|
181
|
+
constructor(tableName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader, pollInterval) {
|
|
181
182
|
this.tableName = tableName;
|
|
182
183
|
this.executor = executor;
|
|
183
184
|
this.singleExecutor = singleExecutor;
|
|
@@ -185,6 +186,7 @@ class QueryBuilder {
|
|
|
185
186
|
this.conditionResolver = conditionResolver ?? null;
|
|
186
187
|
this.revisionGetter = revisionGetter ?? null;
|
|
187
188
|
this.eagerLoader = eagerLoader ?? null;
|
|
189
|
+
this.defaultPollInterval = pollInterval ?? 500;
|
|
188
190
|
this.iqo = {
|
|
189
191
|
selects: [],
|
|
190
192
|
wheres: [],
|
|
@@ -357,7 +359,7 @@ class QueryBuilder {
|
|
|
357
359
|
return results[0]?.count ?? 0;
|
|
358
360
|
}
|
|
359
361
|
subscribe(callback, options = {}) {
|
|
360
|
-
const { interval =
|
|
362
|
+
const { interval = this.defaultPollInterval, immediate = true } = options;
|
|
361
363
|
const fingerprintSQL = this.buildFingerprintSQL();
|
|
362
364
|
let lastFingerprint = null;
|
|
363
365
|
const poll = () => {
|
|
@@ -4686,6 +4688,7 @@ class _Database {
|
|
|
4686
4688
|
schemas;
|
|
4687
4689
|
relationships;
|
|
4688
4690
|
options;
|
|
4691
|
+
pollInterval;
|
|
4689
4692
|
_revisions = {};
|
|
4690
4693
|
constructor(dbFile, schemas, options = {}) {
|
|
4691
4694
|
this.db = new SqliteDatabase(dbFile);
|
|
@@ -4693,6 +4696,7 @@ class _Database {
|
|
|
4693
4696
|
this.db.run("PRAGMA foreign_keys = ON");
|
|
4694
4697
|
this.schemas = schemas;
|
|
4695
4698
|
this.options = options;
|
|
4699
|
+
this.pollInterval = options.pollInterval ?? 500;
|
|
4696
4700
|
this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
|
|
4697
4701
|
this.initializeTables();
|
|
4698
4702
|
this.runMigrations();
|
|
@@ -4710,7 +4714,7 @@ class _Database {
|
|
|
4710
4714
|
upsert: (conditions, data) => this.upsert(entityName, data, conditions),
|
|
4711
4715
|
delete: (id) => this.delete(entityName, id),
|
|
4712
4716
|
select: (...cols) => this._createQueryBuilder(entityName, cols),
|
|
4713
|
-
on: (callback, options2) => this._createOnStream(entityName, callback, options2),
|
|
4717
|
+
on: (callback, options2) => this._createOnStream(entityName, callback, options2?.interval),
|
|
4714
4718
|
_tableName: entityName
|
|
4715
4719
|
};
|
|
4716
4720
|
this[key] = accessor;
|
|
@@ -4760,24 +4764,34 @@ class _Database {
|
|
|
4760
4764
|
const dataVersion = this.db.query("PRAGMA data_version").get()?.data_version ?? 0;
|
|
4761
4765
|
return `${rev}:${dataVersion}`;
|
|
4762
4766
|
}
|
|
4763
|
-
_createOnStream(entityName, callback,
|
|
4764
|
-
const
|
|
4767
|
+
_createOnStream(entityName, callback, intervalOverride) {
|
|
4768
|
+
const interval = intervalOverride ?? this.pollInterval;
|
|
4765
4769
|
const maxRow = this.db.query(`SELECT MAX(id) as _max FROM "${entityName}"`).get();
|
|
4766
4770
|
let lastMaxId = maxRow?._max ?? 0;
|
|
4767
4771
|
let lastRevision = this._getRevision(entityName);
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
if (
|
|
4772
|
+
let stopped = false;
|
|
4773
|
+
const poll = async () => {
|
|
4774
|
+
if (stopped)
|
|
4771
4775
|
return;
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
const
|
|
4776
|
-
|
|
4777
|
-
|
|
4776
|
+
const currentRevision = this._getRevision(entityName);
|
|
4777
|
+
if (currentRevision !== lastRevision) {
|
|
4778
|
+
lastRevision = currentRevision;
|
|
4779
|
+
const newRows = this.db.query(`SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`).all(lastMaxId);
|
|
4780
|
+
for (const rawRow of newRows) {
|
|
4781
|
+
if (stopped)
|
|
4782
|
+
return;
|
|
4783
|
+
const entity = this._attachMethods(entityName, transformFromStorage(rawRow, this.schemas[entityName]));
|
|
4784
|
+
await callback(entity);
|
|
4785
|
+
lastMaxId = rawRow.id;
|
|
4786
|
+
}
|
|
4778
4787
|
}
|
|
4779
|
-
|
|
4780
|
-
|
|
4788
|
+
if (!stopped)
|
|
4789
|
+
setTimeout(poll, interval);
|
|
4790
|
+
};
|
|
4791
|
+
setTimeout(poll, interval);
|
|
4792
|
+
return () => {
|
|
4793
|
+
stopped = true;
|
|
4794
|
+
};
|
|
4781
4795
|
}
|
|
4782
4796
|
insert(entityName, data) {
|
|
4783
4797
|
const schema = this.schemas[entityName];
|
|
@@ -5034,7 +5048,7 @@ class _Database {
|
|
|
5034
5048
|
}
|
|
5035
5049
|
return null;
|
|
5036
5050
|
};
|
|
5037
|
-
const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader);
|
|
5051
|
+
const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader, this.pollInterval);
|
|
5038
5052
|
if (initialCols.length > 0)
|
|
5039
5053
|
builder.select(...initialCols);
|
|
5040
5054
|
return builder;
|
package/package.json
CHANGED
package/src/database.ts
CHANGED
|
@@ -29,6 +29,7 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
29
29
|
private schemas: Schemas;
|
|
30
30
|
private relationships: Relationship[];
|
|
31
31
|
private options: DatabaseOptions;
|
|
32
|
+
private pollInterval: number;
|
|
32
33
|
|
|
33
34
|
/** In-memory revision counter per table — bumps on every write (insert/update/delete).
|
|
34
35
|
* Used by QueryBuilder.subscribe() fingerprint to detect ALL changes with zero overhead. */
|
|
@@ -40,6 +41,7 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
40
41
|
this.db.run('PRAGMA foreign_keys = ON');
|
|
41
42
|
this.schemas = schemas;
|
|
42
43
|
this.options = options;
|
|
44
|
+
this.pollInterval = options.pollInterval ?? 500;
|
|
43
45
|
this.relationships = options.relations ? parseRelationsConfig(options.relations, schemas) : [];
|
|
44
46
|
this.initializeTables();
|
|
45
47
|
this.runMigrations();
|
|
@@ -57,8 +59,8 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
57
59
|
upsert: (conditions, data) => this.upsert(entityName, data, conditions),
|
|
58
60
|
delete: (id) => this.delete(entityName, id),
|
|
59
61
|
select: (...cols: string[]) => this._createQueryBuilder(entityName, cols),
|
|
60
|
-
on: (callback: (row: any) => void
|
|
61
|
-
this._createOnStream(entityName, callback, options),
|
|
62
|
+
on: (callback: (row: any) => void | Promise<void>, options?: { interval?: number }) =>
|
|
63
|
+
this._createOnStream(entityName, callback, options?.interval),
|
|
62
64
|
_tableName: entityName,
|
|
63
65
|
};
|
|
64
66
|
(this as any)[key] = accessor;
|
|
@@ -151,39 +153,52 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
151
153
|
*/
|
|
152
154
|
public _createOnStream(
|
|
153
155
|
entityName: string,
|
|
154
|
-
callback: (row: any) => void
|
|
155
|
-
|
|
156
|
+
callback: (row: any) => void | Promise<void>,
|
|
157
|
+
intervalOverride?: number,
|
|
156
158
|
): () => void {
|
|
157
|
-
const
|
|
159
|
+
const interval = intervalOverride ?? this.pollInterval;
|
|
158
160
|
|
|
159
161
|
// Initialize watermark to current max id (only emit NEW rows)
|
|
160
162
|
const maxRow = this.db.query(`SELECT MAX(id) as _max FROM "${entityName}"`).get() as any;
|
|
161
163
|
let lastMaxId: number = maxRow?._max ?? 0;
|
|
162
164
|
let lastRevision: string = this._getRevision(entityName);
|
|
165
|
+
let stopped = false;
|
|
166
|
+
|
|
167
|
+
// Self-scheduling async loop: guarantees strict ordering
|
|
168
|
+
// - Each callback (sync or async) completes before the next row is emitted
|
|
169
|
+
// - Next poll only starts after the current batch is fully processed
|
|
170
|
+
const poll = async () => {
|
|
171
|
+
if (stopped) return;
|
|
163
172
|
|
|
164
|
-
const timer = setInterval(() => {
|
|
165
173
|
// Fast check: did anything change?
|
|
166
174
|
const currentRevision = this._getRevision(entityName);
|
|
167
|
-
if (currentRevision
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
175
|
+
if (currentRevision !== lastRevision) {
|
|
176
|
+
lastRevision = currentRevision;
|
|
177
|
+
|
|
178
|
+
// Fetch new rows since watermark
|
|
179
|
+
const newRows = this.db.query(
|
|
180
|
+
`SELECT * FROM "${entityName}" WHERE id > ? ORDER BY id ASC`
|
|
181
|
+
).all(lastMaxId) as any[];
|
|
182
|
+
|
|
183
|
+
for (const rawRow of newRows) {
|
|
184
|
+
if (stopped) return; // bail if unsubscribed mid-batch
|
|
185
|
+
const entity = this._attachMethods(
|
|
186
|
+
entityName,
|
|
187
|
+
transformFromStorage(rawRow, this.schemas[entityName]!)
|
|
188
|
+
);
|
|
189
|
+
await callback(entity); // await async callbacks
|
|
190
|
+
lastMaxId = rawRow.id;
|
|
191
|
+
}
|
|
183
192
|
}
|
|
184
|
-
}, interval);
|
|
185
193
|
|
|
186
|
-
|
|
194
|
+
// Schedule next poll only after this one is done
|
|
195
|
+
if (!stopped) setTimeout(poll, interval);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Start the loop
|
|
199
|
+
setTimeout(poll, interval);
|
|
200
|
+
|
|
201
|
+
return () => { stopped = true; };
|
|
187
202
|
}
|
|
188
203
|
|
|
189
204
|
// ===========================================================================
|
|
@@ -534,7 +549,7 @@ class _Database<Schemas extends SchemaMap> {
|
|
|
534
549
|
return null;
|
|
535
550
|
};
|
|
536
551
|
|
|
537
|
-
const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader);
|
|
552
|
+
const builder = new QueryBuilder(entityName, executor, singleExecutor, joinResolver, conditionResolver, revisionGetter, eagerLoader, this.pollInterval);
|
|
538
553
|
if (initialCols.length > 0) builder.select(...initialCols);
|
|
539
554
|
return builder;
|
|
540
555
|
}
|
package/src/query-builder.ts
CHANGED
|
@@ -174,6 +174,7 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
174
174
|
private conditionResolver: ((conditions: Record<string, any>) => Record<string, any>) | null;
|
|
175
175
|
private revisionGetter: (() => string) | null;
|
|
176
176
|
private eagerLoader: ((parentTable: string, relation: string, parentIds: number[]) => { key: string; groups: Map<number, any[]> } | null) | null;
|
|
177
|
+
private defaultPollInterval: number;
|
|
177
178
|
|
|
178
179
|
constructor(
|
|
179
180
|
tableName: string,
|
|
@@ -183,6 +184,7 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
183
184
|
conditionResolver?: ((conditions: Record<string, any>) => Record<string, any>) | null,
|
|
184
185
|
revisionGetter?: (() => string) | null,
|
|
185
186
|
eagerLoader?: ((parentTable: string, relation: string, parentIds: number[]) => { key: string; groups: Map<number, any[]> } | null) | null,
|
|
187
|
+
pollInterval?: number,
|
|
186
188
|
) {
|
|
187
189
|
this.tableName = tableName;
|
|
188
190
|
this.executor = executor;
|
|
@@ -191,6 +193,7 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
191
193
|
this.conditionResolver = conditionResolver ?? null;
|
|
192
194
|
this.revisionGetter = revisionGetter ?? null;
|
|
193
195
|
this.eagerLoader = eagerLoader ?? null;
|
|
196
|
+
this.defaultPollInterval = pollInterval ?? 500;
|
|
194
197
|
this.iqo = {
|
|
195
198
|
selects: [],
|
|
196
199
|
wheres: [],
|
|
@@ -497,7 +500,7 @@ export class QueryBuilder<T extends Record<string, any>> {
|
|
|
497
500
|
callback: (rows: T[]) => void,
|
|
498
501
|
options: { interval?: number; immediate?: boolean } = {},
|
|
499
502
|
): () => void {
|
|
500
|
-
const { interval =
|
|
503
|
+
const { interval = this.defaultPollInterval, immediate = true } = options;
|
|
501
504
|
|
|
502
505
|
// Build the fingerprint SQL (COUNT + MAX(id)) using the same WHERE
|
|
503
506
|
const fingerprintSQL = this.buildFingerprintSQL();
|
package/src/types.ts
CHANGED
|
@@ -30,6 +30,11 @@ export type DatabaseOptions<R extends RelationsConfig = RelationsConfig> = {
|
|
|
30
30
|
* `books: { author_id: 'authors' }` → FOREIGN KEY, lazy nav, fluent join.
|
|
31
31
|
*/
|
|
32
32
|
relations?: R;
|
|
33
|
+
/**
|
|
34
|
+
* Global polling interval (ms) for `.on()` and `.subscribe()`.
|
|
35
|
+
* Can be overridden per-call. Default: 500ms.
|
|
36
|
+
*/
|
|
37
|
+
pollInterval?: number;
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
export type Relationship = {
|
|
@@ -146,9 +151,10 @@ export type NavEntityAccessor<
|
|
|
146
151
|
/**
|
|
147
152
|
* Stream new rows one at a time, in insertion order.
|
|
148
153
|
* Only emits rows inserted AFTER subscription starts.
|
|
154
|
+
* Callbacks are awaited — strict ordering guaranteed even with async handlers.
|
|
149
155
|
* @returns Unsubscribe function.
|
|
150
156
|
*/
|
|
151
|
-
on: (callback: (row: NavEntity<S, R, Table>) => void
|
|
157
|
+
on: (callback: (row: NavEntity<S, R, Table>) => void | Promise<void>, options?: { interval?: number }) => () => void;
|
|
152
158
|
_tableName: string;
|
|
153
159
|
readonly _schema?: S[Table & keyof S];
|
|
154
160
|
};
|
|
@@ -177,9 +183,10 @@ export type EntityAccessor<S extends z.ZodType<any>> = {
|
|
|
177
183
|
/**
|
|
178
184
|
* Stream new rows one at a time, in insertion order.
|
|
179
185
|
* Only emits rows inserted AFTER subscription starts.
|
|
186
|
+
* Callbacks are awaited — strict ordering guaranteed even with async handlers.
|
|
180
187
|
* @returns Unsubscribe function.
|
|
181
188
|
*/
|
|
182
|
-
on: (callback: (row: AugmentedEntity<S>) => void
|
|
189
|
+
on: (callback: (row: AugmentedEntity<S>) => void | Promise<void>, options?: { interval?: number }) => () => void;
|
|
183
190
|
_tableName: string;
|
|
184
191
|
readonly _schema?: S;
|
|
185
192
|
};
|