@monlite/core 0.7.0 → 0.9.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 +63 -0
- package/dist/index.cjs +226 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +210 -80
- package/dist/index.d.ts +210 -80
- package/dist/index.js +226 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -319,6 +319,31 @@ await users.distinct("tags"); // ["a", "b", "c"]
|
|
|
319
319
|
|
|
320
320
|
---
|
|
321
321
|
|
|
322
|
+
## Live queries (reactivity)
|
|
323
|
+
|
|
324
|
+
`collection.watch()` keeps a query result live. The callback fires once
|
|
325
|
+
immediately (`type: "init"`) and again whenever a change **affects this query** —
|
|
326
|
+
matching is **row-level**, so unrelated writes don't trigger a recompute. It also
|
|
327
|
+
fires for changes applied by `@monlite/sync`, so the UI updates when cloud data
|
|
328
|
+
arrives.
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
const handle = users.watch({ where: { role: "admin" } }, (event) => {
|
|
332
|
+
event.results; // full current result set
|
|
333
|
+
event.added; // docs that just entered the set
|
|
334
|
+
event.removed; // docs that just left
|
|
335
|
+
event.changed; // docs still in the set whose contents changed
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
handle.results; // current results, kept up to date
|
|
339
|
+
handle.stop(); // unsubscribe
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Perfect for Electron/Tauri UIs: bind `handle.results` to your view and it stays
|
|
343
|
+
in sync with every write (local or synced).
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
322
347
|
## Structured collections (native SQL columns)
|
|
323
348
|
|
|
324
349
|
By default a collection is **document mode** — schema-free, every field stored
|
|
@@ -362,6 +387,11 @@ await db.$queryRaw`
|
|
|
362
387
|
Column types: `"TEXT" | "INTEGER" | "REAL" | "BLOB" | "JSON"`. A full column
|
|
363
388
|
definition supports `index`, `unique`, `notNull`, `default`, and `references`.
|
|
364
389
|
|
|
390
|
+
**Migrations are automatic for additive changes.** Re-opening a collection with a
|
|
391
|
+
new declared column adds it (`ALTER TABLE ADD COLUMN`) on declaration — give
|
|
392
|
+
`NOT NULL` columns a `default` so existing rows can be backfilled. Destructive
|
|
393
|
+
changes (rename/drop/type-change) still need a manual migration.
|
|
394
|
+
|
|
365
395
|
### Do I have to care: JSON vs native columns?
|
|
366
396
|
|
|
367
397
|
- **For correctness — no.** Both modes return identical results through the same API.
|
|
@@ -472,6 +502,13 @@ ON users(json_extract(data, '$.address.city'));
|
|
|
472
502
|
|
|
473
503
|
You never think about indexes. Disable with `createDb("./app.db", { autoIndex: false })`.
|
|
474
504
|
|
|
505
|
+
Want to see whether a query uses an index? Ask:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
await users.explain({ where: { "address.city": "Riyadh" } });
|
|
509
|
+
// { sql, usesIndex: true, plan: [{ id, parent, detail }, …] }
|
|
510
|
+
```
|
|
511
|
+
|
|
475
512
|
---
|
|
476
513
|
|
|
477
514
|
## Database management
|
|
@@ -480,6 +517,7 @@ You never think about indexes. Disable with `createDb("./app.db", { autoIndex: f
|
|
|
480
517
|
await db.$collections(); // string[] of collection names
|
|
481
518
|
await db.$drop("users"); // drop a collection and its data
|
|
482
519
|
await db.$dropAll(); // drop everything
|
|
520
|
+
await db.backup("./snapshot.db"); // consistent on-disk snapshot
|
|
483
521
|
await db.$disconnect(); // close the connection
|
|
484
522
|
db.sqlite; // the underlying native driver handle
|
|
485
523
|
db.driverName; // "better-sqlite3" | "node:sqlite"
|
|
@@ -487,6 +525,31 @@ db.driverName; // "better-sqlite3" | "node:sqlite"
|
|
|
487
525
|
|
|
488
526
|
---
|
|
489
527
|
|
|
528
|
+
## Plugins
|
|
529
|
+
|
|
530
|
+
`@monlite/core` stays lean; heavier or optional capabilities are opt-in plugins
|
|
531
|
+
passed to `createDb`:
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
import { createDb } from "@monlite/core";
|
|
535
|
+
import { fts } from "@monlite/fts";
|
|
536
|
+
|
|
537
|
+
const db = createDb("./app.db", {
|
|
538
|
+
plugins: [fts({ posts: ["title", "body"] })],
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
await db.collection("posts").search("hello world"); // full-text search
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
| Plugin | Adds |
|
|
545
|
+
|---|---|
|
|
546
|
+
| [`@monlite/fts`](https://www.npmjs.com/package/@monlite/fts) | Full-text search (SQLite FTS5) via `collection.search()` |
|
|
547
|
+
|
|
548
|
+
Write your own against the `MonlitePlugin` interface (`init` / `afterWrite` /
|
|
549
|
+
`collectionMethods` hooks).
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
490
553
|
## Drivers & zero dependencies
|
|
491
554
|
|
|
492
555
|
monlite talks to SQLite through a tiny driver adapter, so it runs on two
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,91 @@ function isObjectId(value) {
|
|
|
20
20
|
return typeof value === "string" && /^[0-9a-f]{24}$/i.test(value);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/reactive.ts
|
|
24
|
+
var RELEVANCE_PROBE_LIMIT = 500;
|
|
25
|
+
var LiveQuery = class {
|
|
26
|
+
constructor(source, args, cb) {
|
|
27
|
+
this.source = source;
|
|
28
|
+
this.args = args;
|
|
29
|
+
this.cb = cb;
|
|
30
|
+
this.recompute("init");
|
|
31
|
+
}
|
|
32
|
+
source;
|
|
33
|
+
args;
|
|
34
|
+
cb;
|
|
35
|
+
results = [];
|
|
36
|
+
stopped = false;
|
|
37
|
+
ids = /* @__PURE__ */ new Set();
|
|
38
|
+
/** Called by the Reactor with the ids that changed this tick. */
|
|
39
|
+
notify(changedIds) {
|
|
40
|
+
if (this.stopped) return;
|
|
41
|
+
if (!this.isRelevant(changedIds)) return;
|
|
42
|
+
this.recompute("change", changedIds);
|
|
43
|
+
}
|
|
44
|
+
isRelevant(changedIds) {
|
|
45
|
+
for (const id of changedIds) {
|
|
46
|
+
if (this.ids.has(id)) return true;
|
|
47
|
+
}
|
|
48
|
+
if (changedIds.size > RELEVANCE_PROBE_LIMIT) return true;
|
|
49
|
+
const idIn = {
|
|
50
|
+
_id: { in: [...changedIds] }
|
|
51
|
+
};
|
|
52
|
+
const where = this.args.where ? { AND: [this.args.where, idIn] } : idIn;
|
|
53
|
+
return this.source.existsCore(where);
|
|
54
|
+
}
|
|
55
|
+
recompute(type, changedIds) {
|
|
56
|
+
const next = this.source.findManyCore(this.args);
|
|
57
|
+
const nextById = new Map(next.map((d) => [d._id, d]));
|
|
58
|
+
const added = next.filter((d) => !this.ids.has(d._id));
|
|
59
|
+
const removed = this.results.filter((d) => !nextById.has(d._id));
|
|
60
|
+
const changed = changedIds ? next.filter((d) => this.ids.has(d._id) && changedIds.has(d._id)) : [];
|
|
61
|
+
this.results = next;
|
|
62
|
+
this.ids = new Set(nextById.keys());
|
|
63
|
+
this.cb({ type, results: next, added, removed, changed });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var Reactor = class {
|
|
67
|
+
byCollection = /* @__PURE__ */ new Map();
|
|
68
|
+
pending = /* @__PURE__ */ new Map();
|
|
69
|
+
flushScheduled = false;
|
|
70
|
+
hasWatchers(collection) {
|
|
71
|
+
return this.byCollection.has(collection);
|
|
72
|
+
}
|
|
73
|
+
register(collection, lq) {
|
|
74
|
+
let set = this.byCollection.get(collection);
|
|
75
|
+
if (!set) this.byCollection.set(collection, set = /* @__PURE__ */ new Set());
|
|
76
|
+
set.add(lq);
|
|
77
|
+
}
|
|
78
|
+
unregister(collection, lq) {
|
|
79
|
+
const set = this.byCollection.get(collection);
|
|
80
|
+
if (!set) return;
|
|
81
|
+
set.delete(lq);
|
|
82
|
+
if (set.size === 0) this.byCollection.delete(collection);
|
|
83
|
+
}
|
|
84
|
+
/** Record that documents changed; schedule a notification flush. */
|
|
85
|
+
emit(collection, ids) {
|
|
86
|
+
const set = this.byCollection.get(collection);
|
|
87
|
+
if (!set || set.size === 0 || ids.length === 0) return;
|
|
88
|
+
let p = this.pending.get(collection);
|
|
89
|
+
if (!p) this.pending.set(collection, p = /* @__PURE__ */ new Set());
|
|
90
|
+
for (const id of ids) p.add(id);
|
|
91
|
+
if (!this.flushScheduled) {
|
|
92
|
+
this.flushScheduled = true;
|
|
93
|
+
queueMicrotask(() => this.flush());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
flush() {
|
|
97
|
+
this.flushScheduled = false;
|
|
98
|
+
const work = [...this.pending];
|
|
99
|
+
this.pending.clear();
|
|
100
|
+
for (const [collection, ids] of work) {
|
|
101
|
+
const set = this.byCollection.get(collection);
|
|
102
|
+
if (!set) continue;
|
|
103
|
+
for (const lq of [...set]) lq.notify(ids);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
23
108
|
// src/errors.ts
|
|
24
109
|
var MonliteError = class extends Error {
|
|
25
110
|
constructor(message, options) {
|
|
@@ -605,6 +690,7 @@ var Collection = class {
|
|
|
605
690
|
this.columns.add(field);
|
|
606
691
|
if (normalized.type === "JSON") this.jsonColumns.add(field);
|
|
607
692
|
}
|
|
693
|
+
this.ensureTable();
|
|
608
694
|
}
|
|
609
695
|
}
|
|
610
696
|
mon;
|
|
@@ -649,23 +735,15 @@ var Collection = class {
|
|
|
649
735
|
`_id TEXT PRIMARY KEY`,
|
|
650
736
|
`created_at INTEGER NOT NULL`,
|
|
651
737
|
`updated_at INTEGER NOT NULL`,
|
|
652
|
-
`data TEXT NOT NULL DEFAULT '{}'
|
|
738
|
+
`data TEXT NOT NULL DEFAULT '{}'`,
|
|
739
|
+
...this.columnOrder.map((f) => this.columnDdl(f, false))
|
|
653
740
|
];
|
|
654
|
-
for (const field of this.columnOrder) {
|
|
655
|
-
const def = this.columnDefs[field];
|
|
656
|
-
let line = `"${field}" ${sqliteType(def.type)}`;
|
|
657
|
-
if (def.notNull) line += " NOT NULL";
|
|
658
|
-
if (def.unique) line += " UNIQUE";
|
|
659
|
-
if (def.default !== void 0)
|
|
660
|
-
line += ` DEFAULT ${formatDefault(def.default)}`;
|
|
661
|
-
if (def.references) line += ` REFERENCES ${def.references}`;
|
|
662
|
-
lines.push(line);
|
|
663
|
-
}
|
|
664
741
|
this.db.exec(
|
|
665
742
|
`CREATE TABLE IF NOT EXISTS "${this.name}" (
|
|
666
743
|
${lines.join(",\n ")}
|
|
667
744
|
)`
|
|
668
745
|
);
|
|
746
|
+
this.migrateColumns();
|
|
669
747
|
for (const field of this.columnOrder) {
|
|
670
748
|
if (this.columnDefs[field].index) {
|
|
671
749
|
this.db.exec(
|
|
@@ -676,6 +754,39 @@ var Collection = class {
|
|
|
676
754
|
}
|
|
677
755
|
this.initialized = true;
|
|
678
756
|
}
|
|
757
|
+
columnDdl(field, forAlter) {
|
|
758
|
+
const def = this.columnDefs[field];
|
|
759
|
+
let line = `"${field}" ${sqliteType(def.type)}`;
|
|
760
|
+
if (def.notNull) line += " NOT NULL";
|
|
761
|
+
if (def.unique && !forAlter) line += " UNIQUE";
|
|
762
|
+
if (def.default !== void 0)
|
|
763
|
+
line += ` DEFAULT ${formatDefault(def.default)}`;
|
|
764
|
+
if (def.references) line += ` REFERENCES ${def.references}`;
|
|
765
|
+
return line;
|
|
766
|
+
}
|
|
767
|
+
/** Auto-additive migration: add declared columns missing from an existing table. */
|
|
768
|
+
migrateColumns() {
|
|
769
|
+
const existing = new Set(
|
|
770
|
+
this.db.prepare(`PRAGMA table_info("${this.name}")`).all().map((r) => r.name)
|
|
771
|
+
);
|
|
772
|
+
for (const field of this.columnOrder) {
|
|
773
|
+
if (existing.has(field)) continue;
|
|
774
|
+
try {
|
|
775
|
+
this.db.exec(
|
|
776
|
+
`ALTER TABLE "${this.name}" ADD COLUMN ${this.columnDdl(field, true)}`
|
|
777
|
+
);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
throw new MonliteError(
|
|
780
|
+
`Failed to add column "${field}" to "${this.name}": ${err.message}. NOT NULL columns need a default when added to an existing table.`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
if (this.columnDefs[field].unique) {
|
|
784
|
+
this.db.exec(
|
|
785
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_${this.name}_${field}" ON "${this.name}"("${field}")`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
679
790
|
/* --------------------------- row <-> doc -------------------------- */
|
|
680
791
|
rowToDoc(row) {
|
|
681
792
|
const doc = this.mode === "document" ? JSON.parse(row.data) : JSON.parse(row.data ?? "{}");
|
|
@@ -798,6 +909,7 @@ var Collection = class {
|
|
|
798
909
|
this.ensureTable();
|
|
799
910
|
if (op === "delete") {
|
|
800
911
|
this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`).run(id);
|
|
912
|
+
this.afterWrite([id]);
|
|
801
913
|
return;
|
|
802
914
|
}
|
|
803
915
|
const clean = stripSystem(doc ?? {});
|
|
@@ -807,6 +919,7 @@ var Collection = class {
|
|
|
807
919
|
`INSERT INTO "${this.name}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
808
920
|
ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
|
809
921
|
).run(id, JSON.stringify(clean), createdAt, ts);
|
|
922
|
+
this.afterWrite([id]);
|
|
810
923
|
return;
|
|
811
924
|
}
|
|
812
925
|
const overflow = {};
|
|
@@ -837,6 +950,13 @@ var Collection = class {
|
|
|
837
950
|
this.db.prepare(
|
|
838
951
|
`INSERT INTO "${this.name}" (${colList}) VALUES (${placeholders}) ON CONFLICT(_id) DO UPDATE SET ${updateSet}`
|
|
839
952
|
).run(...values);
|
|
953
|
+
this.afterWrite([id]);
|
|
954
|
+
}
|
|
955
|
+
/** @internal Notify reactivity watchers and plugins that documents changed. */
|
|
956
|
+
afterWrite(ids) {
|
|
957
|
+
if (ids.length === 0) return;
|
|
958
|
+
this.mon.reactor.emit(this.name, ids);
|
|
959
|
+
this.mon.firePluginAfterWrite(this.name, ids);
|
|
840
960
|
}
|
|
841
961
|
/* ----------------------------- create ----------------------------- */
|
|
842
962
|
async create(args) {
|
|
@@ -848,26 +968,29 @@ var Collection = class {
|
|
|
848
968
|
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
849
969
|
};
|
|
850
970
|
this.guard(() => recorder ? this.db.transaction(write) : write());
|
|
971
|
+
this.afterWrite([row._id]);
|
|
851
972
|
return row.returned;
|
|
852
973
|
}
|
|
853
974
|
async createMany(args) {
|
|
854
975
|
this.ensureTable();
|
|
855
976
|
const stmt = this.db.prepare(this.insertSql());
|
|
856
977
|
const recorder = this.recorder;
|
|
978
|
+
const ids = [];
|
|
857
979
|
this.guard(
|
|
858
980
|
() => this.db.transaction(() => {
|
|
859
981
|
for (const item of args.data) {
|
|
860
982
|
const row = this.buildInsert(item);
|
|
861
983
|
stmt.run(...row.values);
|
|
862
984
|
recorder?.recordLocal(this.name, row._id, "upsert", row.created_at);
|
|
985
|
+
ids.push(row._id);
|
|
863
986
|
}
|
|
864
987
|
})
|
|
865
988
|
);
|
|
989
|
+
this.afterWrite(ids);
|
|
866
990
|
return { count: args.data.length };
|
|
867
991
|
}
|
|
868
992
|
/* ------------------------------ read ------------------------------ */
|
|
869
|
-
|
|
870
|
-
this.ensureTable();
|
|
993
|
+
buildFindSql(args) {
|
|
871
994
|
const params = [];
|
|
872
995
|
const where = buildWhere(args.where, {
|
|
873
996
|
params,
|
|
@@ -885,9 +1008,29 @@ var Collection = class {
|
|
|
885
1008
|
sql += (args.take != null ? "" : " LIMIT -1") + " OFFSET ?";
|
|
886
1009
|
params.push(args.skip);
|
|
887
1010
|
}
|
|
1011
|
+
return { sql, params };
|
|
1012
|
+
}
|
|
1013
|
+
/** @internal Synchronous core of findMany (used by reactivity). */
|
|
1014
|
+
findManyCore(args = {}) {
|
|
1015
|
+
this.ensureTable();
|
|
1016
|
+
const { sql, params } = this.buildFindSql(args);
|
|
888
1017
|
const rows = this.db.prepare(sql).all(...params);
|
|
889
1018
|
return rows.map((r) => project(this.rowToDoc(r), args.select));
|
|
890
1019
|
}
|
|
1020
|
+
/** @internal Synchronous core of exists (used by reactivity). */
|
|
1021
|
+
existsCore(where) {
|
|
1022
|
+
this.ensureTable();
|
|
1023
|
+
const params = [];
|
|
1024
|
+
const clause = buildWhere(where, {
|
|
1025
|
+
params,
|
|
1026
|
+
onPath: this.trackPath,
|
|
1027
|
+
columns: this.columns
|
|
1028
|
+
});
|
|
1029
|
+
return this.db.prepare(`SELECT 1 FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params) != null;
|
|
1030
|
+
}
|
|
1031
|
+
async findMany(args = {}) {
|
|
1032
|
+
return this.findManyCore(args);
|
|
1033
|
+
}
|
|
891
1034
|
async findFirst(args = {}) {
|
|
892
1035
|
const rows = await this.findMany({ ...args, take: 1 });
|
|
893
1036
|
return rows[0] ?? null;
|
|
@@ -904,15 +1047,39 @@ var Collection = class {
|
|
|
904
1047
|
}
|
|
905
1048
|
/** True if at least one document matches. */
|
|
906
1049
|
async exists(where) {
|
|
1050
|
+
return this.existsCore(where);
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Subscribe to a live query. The callback fires immediately with the current
|
|
1054
|
+
* results (`type: "init"`) and again whenever a change affects the result set
|
|
1055
|
+
* (row-level: only relevant changes trigger a recompute). Includes changes
|
|
1056
|
+
* applied by `@monlite/sync`.
|
|
1057
|
+
*/
|
|
1058
|
+
watch(args = {}, cb) {
|
|
907
1059
|
this.ensureTable();
|
|
908
|
-
const
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1060
|
+
const lq = new LiveQuery(this, args, cb);
|
|
1061
|
+
const reactor = this.mon.reactor;
|
|
1062
|
+
const name = this.name;
|
|
1063
|
+
reactor.register(name, lq);
|
|
1064
|
+
return {
|
|
1065
|
+
get results() {
|
|
1066
|
+
return lq.results;
|
|
1067
|
+
},
|
|
1068
|
+
stop() {
|
|
1069
|
+
lq.stopped = true;
|
|
1070
|
+
reactor.unregister(name, lq);
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
/** Show SQLite's query plan for a `findMany`, and whether it uses an index. */
|
|
1075
|
+
async explain(args = {}) {
|
|
1076
|
+
this.ensureTable();
|
|
1077
|
+
const { sql, params } = this.buildFindSql(args);
|
|
1078
|
+
const plan = this.db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...params);
|
|
1079
|
+
const usesIndex = plan.some(
|
|
1080
|
+
(r) => /USING (COVERING )?INDEX/i.test(r.detail)
|
|
1081
|
+
);
|
|
1082
|
+
return { sql, usesIndex, plan };
|
|
916
1083
|
}
|
|
917
1084
|
async findById(id) {
|
|
918
1085
|
this.ensureTable();
|
|
@@ -969,25 +1136,27 @@ var Collection = class {
|
|
|
969
1136
|
if (!rows.length) return [];
|
|
970
1137
|
const now = Date.now();
|
|
971
1138
|
const recorder = this.recorder;
|
|
972
|
-
|
|
1139
|
+
const out = this.guard(
|
|
973
1140
|
() => this.db.transaction(() => {
|
|
974
|
-
const
|
|
1141
|
+
const result = [];
|
|
975
1142
|
for (const row of rows) {
|
|
976
1143
|
const current = stripSystem(this.rowToDoc(row));
|
|
977
1144
|
const updated = stripSystem(applyUpdate(current, data));
|
|
978
1145
|
const { setSql, values } = this.buildUpdateSet(updated, now);
|
|
979
1146
|
this.db.prepare(`UPDATE "${this.name}" SET ${setSql} WHERE _id = ?`).run(...values, row._id);
|
|
980
1147
|
recorder?.recordLocal(this.name, row._id, "upsert", now);
|
|
981
|
-
|
|
1148
|
+
result.push({
|
|
982
1149
|
...updated,
|
|
983
1150
|
_id: row._id,
|
|
984
1151
|
created_at: row.created_at,
|
|
985
1152
|
updated_at: now
|
|
986
1153
|
});
|
|
987
1154
|
}
|
|
988
|
-
return
|
|
1155
|
+
return result;
|
|
989
1156
|
})
|
|
990
1157
|
);
|
|
1158
|
+
this.afterWrite(out.map((d) => d._id));
|
|
1159
|
+
return out;
|
|
991
1160
|
}
|
|
992
1161
|
async update(args) {
|
|
993
1162
|
return this.runUpdate(args.where, args.data, true)[0] ?? null;
|
|
@@ -997,7 +1166,7 @@ var Collection = class {
|
|
|
997
1166
|
}
|
|
998
1167
|
async upsert(args) {
|
|
999
1168
|
this.ensureTable();
|
|
1000
|
-
|
|
1169
|
+
const result = this.guard(
|
|
1001
1170
|
() => this.db.transaction(() => {
|
|
1002
1171
|
const params = [];
|
|
1003
1172
|
const clause = buildWhere(args.where, {
|
|
@@ -1027,6 +1196,8 @@ var Collection = class {
|
|
|
1027
1196
|
return ins.returned;
|
|
1028
1197
|
})
|
|
1029
1198
|
);
|
|
1199
|
+
this.afterWrite([result._id]);
|
|
1200
|
+
return result;
|
|
1030
1201
|
}
|
|
1031
1202
|
/* ----------------------------- delete ----------------------------- */
|
|
1032
1203
|
runDelete(where, single) {
|
|
@@ -1052,6 +1223,7 @@ var Collection = class {
|
|
|
1052
1223
|
}
|
|
1053
1224
|
})
|
|
1054
1225
|
);
|
|
1226
|
+
this.afterWrite(rows.map((r) => r._id));
|
|
1055
1227
|
return rows.map((r) => this.rowToDoc(r));
|
|
1056
1228
|
}
|
|
1057
1229
|
async delete(args) {
|
|
@@ -1713,9 +1885,12 @@ var Monlite = class {
|
|
|
1713
1885
|
driver;
|
|
1714
1886
|
/** @internal */
|
|
1715
1887
|
autoIndexer;
|
|
1888
|
+
/** @internal Reactivity hub for `collection.watch()`. */
|
|
1889
|
+
reactor = new Reactor();
|
|
1716
1890
|
/** @internal Sync metadata store; present only when `{ sync: true }`. */
|
|
1717
1891
|
$sync;
|
|
1718
1892
|
collections = /* @__PURE__ */ new Map();
|
|
1893
|
+
plugins;
|
|
1719
1894
|
closed = false;
|
|
1720
1895
|
constructor(filename, options = {}) {
|
|
1721
1896
|
this.driver = createDriver(filename, {
|
|
@@ -1733,6 +1908,15 @@ var Monlite = class {
|
|
|
1733
1908
|
if (options.sync) {
|
|
1734
1909
|
this.$sync = new SyncStore(this.driver, options.nodeId, this);
|
|
1735
1910
|
}
|
|
1911
|
+
this.plugins = options.plugins ?? [];
|
|
1912
|
+
for (const plugin of this.plugins) plugin.init?.(this);
|
|
1913
|
+
}
|
|
1914
|
+
/** @internal Notify plugins that documents changed (post-commit). */
|
|
1915
|
+
firePluginAfterWrite(collection, ids) {
|
|
1916
|
+
if (this.plugins.length === 0 || ids.length === 0) return;
|
|
1917
|
+
for (const plugin of this.plugins) {
|
|
1918
|
+
plugin.afterWrite?.(this, { collection, ids });
|
|
1919
|
+
}
|
|
1736
1920
|
}
|
|
1737
1921
|
/** Stable node id for LWW tie-breaking (only when sync is enabled). */
|
|
1738
1922
|
get nodeId() {
|
|
@@ -1758,6 +1942,13 @@ var Monlite = class {
|
|
|
1758
1942
|
if (!col) {
|
|
1759
1943
|
col = new Collection(this, name, options);
|
|
1760
1944
|
this.collections.set(name, col);
|
|
1945
|
+
for (const plugin of this.plugins) {
|
|
1946
|
+
for (const [method, impl] of Object.entries(
|
|
1947
|
+
plugin.collectionMethods ?? {}
|
|
1948
|
+
)) {
|
|
1949
|
+
col[method] = (...args) => impl(col, ...args);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1761
1952
|
} else if (options?.schema) {
|
|
1762
1953
|
const requested = Object.keys(options.schema);
|
|
1763
1954
|
const existing = new Set(col.columnNames);
|
|
@@ -1847,6 +2038,15 @@ var Monlite = class {
|
|
|
1847
2038
|
async $dropAll() {
|
|
1848
2039
|
for (const name of await this.$collections()) await this.$drop(name);
|
|
1849
2040
|
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Write a consistent on-disk snapshot of the database to `path` (via
|
|
2043
|
+
* `VACUUM INTO`). The destination file must not already exist.
|
|
2044
|
+
*/
|
|
2045
|
+
backup(path) {
|
|
2046
|
+
this.assertOpen();
|
|
2047
|
+
this.driver.exec(`VACUUM INTO '${path.replace(/'/g, "''")}'`);
|
|
2048
|
+
return Promise.resolve();
|
|
2049
|
+
}
|
|
1850
2050
|
/** Close the underlying SQLite connection. */
|
|
1851
2051
|
$disconnect() {
|
|
1852
2052
|
if (!this.closed) {
|