@monlite/core 0.6.0 → 0.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/README.md +4 -2
- package/dist/index.cjs +129 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +129 -23
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -379,8 +379,10 @@ createDb("./app.db", { verbose: (sql) => console.log(sql) }); // see json_extrac
|
|
|
379
379
|
> **Rule of thumb:** unknown/flexible shape → document (JSON); known/stable shape
|
|
380
380
|
> with heavy joins, reporting, or external SQL tooling → structured (native columns).
|
|
381
381
|
|
|
382
|
-
>
|
|
383
|
-
>
|
|
382
|
+
> Both document and structured collections are syncable via
|
|
383
|
+
> [`@monlite/sync`](#sync--local-first). To sync a structured collection, open it
|
|
384
|
+
> with its `schema` on every node before syncing (so each side knows the native
|
|
385
|
+
> columns).
|
|
384
386
|
|
|
385
387
|
---
|
|
386
388
|
|
package/dist/index.cjs
CHANGED
|
@@ -239,9 +239,7 @@ function cmp(expr, op, v, ctx) {
|
|
|
239
239
|
}
|
|
240
240
|
function inExpr(expr, arr, ctx, negate) {
|
|
241
241
|
if (!Array.isArray(arr)) {
|
|
242
|
-
throw new MonliteQueryError(
|
|
243
|
-
`${negate ? "notIn" : "in"} expects an array`
|
|
244
|
-
);
|
|
242
|
+
throw new MonliteQueryError(`${negate ? "notIn" : "in"} expects an array`);
|
|
245
243
|
}
|
|
246
244
|
if (arr.length === 0) return negate ? "1" : "0";
|
|
247
245
|
const placeholders = arr.map((v) => {
|
|
@@ -371,7 +369,8 @@ function applyUpdate(doc, data) {
|
|
|
371
369
|
}
|
|
372
370
|
const ops = data;
|
|
373
371
|
if (ops.$set) {
|
|
374
|
-
for (const [path, value] of Object.entries(ops.$set))
|
|
372
|
+
for (const [path, value] of Object.entries(ops.$set))
|
|
373
|
+
setPath(next, path, value);
|
|
375
374
|
}
|
|
376
375
|
if (ops.$inc) {
|
|
377
376
|
for (const [path, by] of Object.entries(ops.$inc)) {
|
|
@@ -492,7 +491,11 @@ function buildHaving(having, params, columns) {
|
|
|
492
491
|
if (!selection) continue;
|
|
493
492
|
for (const field of Object.keys(selection)) {
|
|
494
493
|
parts.push(
|
|
495
|
-
...comparisonSql(
|
|
494
|
+
...comparisonSql(
|
|
495
|
+
`${fn}(${fieldExpr(field, columns)})`,
|
|
496
|
+
selection[field],
|
|
497
|
+
params
|
|
498
|
+
)
|
|
496
499
|
);
|
|
497
500
|
}
|
|
498
501
|
}
|
|
@@ -520,7 +523,11 @@ function groupBy(ctx, args) {
|
|
|
520
523
|
groupCols.push({ alias, field });
|
|
521
524
|
});
|
|
522
525
|
selects.push(`COUNT(*) AS agg_count`);
|
|
523
|
-
const { selects: accSelects, cols } = buildAccumulators(
|
|
526
|
+
const { selects: accSelects, cols } = buildAccumulators(
|
|
527
|
+
args,
|
|
528
|
+
ctx.onPath,
|
|
529
|
+
ctx.columns
|
|
530
|
+
);
|
|
524
531
|
selects.push(...accSelects);
|
|
525
532
|
let sql = `SELECT ${selects.join(", ")} FROM "${ctx.table}" WHERE ${where} GROUP BY ${groupExprs.join(", ")}`;
|
|
526
533
|
if (args.having) {
|
|
@@ -649,7 +656,8 @@ var Collection = class {
|
|
|
649
656
|
let line = `"${field}" ${sqliteType(def.type)}`;
|
|
650
657
|
if (def.notNull) line += " NOT NULL";
|
|
651
658
|
if (def.unique) line += " UNIQUE";
|
|
652
|
-
if (def.default !== void 0)
|
|
659
|
+
if (def.default !== void 0)
|
|
660
|
+
line += ` DEFAULT ${formatDefault(def.default)}`;
|
|
653
661
|
if (def.references) line += ` REFERENCES ${def.references}`;
|
|
654
662
|
lines.push(line);
|
|
655
663
|
}
|
|
@@ -713,7 +721,12 @@ var Collection = class {
|
|
|
713
721
|
const now = Date.now();
|
|
714
722
|
const id = input._id != null ? String(input._id) : objectId();
|
|
715
723
|
const doc = stripSystem(input);
|
|
716
|
-
const returned = {
|
|
724
|
+
const returned = {
|
|
725
|
+
...doc,
|
|
726
|
+
_id: id,
|
|
727
|
+
created_at: now,
|
|
728
|
+
updated_at: now
|
|
729
|
+
};
|
|
717
730
|
if (this.mode === "document") {
|
|
718
731
|
return {
|
|
719
732
|
_id: id,
|
|
@@ -765,9 +778,65 @@ var Collection = class {
|
|
|
765
778
|
];
|
|
766
779
|
return { setSql: setParts.join(", "), values };
|
|
767
780
|
}
|
|
768
|
-
/** Sync store
|
|
781
|
+
/** Sync store for recording local changes (both document and structured). */
|
|
769
782
|
get recorder() {
|
|
770
|
-
return this.
|
|
783
|
+
return this.mon.$sync;
|
|
784
|
+
}
|
|
785
|
+
/** @internal Read a full document by id (mode-aware), synchronously. */
|
|
786
|
+
getRaw(id) {
|
|
787
|
+
this.ensureTable();
|
|
788
|
+
const row = this.db.prepare(`SELECT * FROM "${this.name}" WHERE _id = ?`).get(id);
|
|
789
|
+
return row ? this.rowToDoc(row) : null;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* @internal Apply a remote change to storage WITHOUT recording it to the
|
|
793
|
+
* change feed (the sync store records the `remote` feed row itself). Used by
|
|
794
|
+
* `@monlite/sync` so structured collections sync correctly through the same
|
|
795
|
+
* column/overflow split as local writes.
|
|
796
|
+
*/
|
|
797
|
+
applyRemoteWrite(op, id, doc, ts) {
|
|
798
|
+
this.ensureTable();
|
|
799
|
+
if (op === "delete") {
|
|
800
|
+
this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`).run(id);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const clean = stripSystem(doc ?? {});
|
|
804
|
+
const createdAt = typeof doc?.created_at === "number" ? doc.created_at : ts;
|
|
805
|
+
if (this.mode === "document") {
|
|
806
|
+
this.db.prepare(
|
|
807
|
+
`INSERT INTO "${this.name}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
808
|
+
ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
|
809
|
+
).run(id, JSON.stringify(clean), createdAt, ts);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const overflow = {};
|
|
813
|
+
const colValues = {};
|
|
814
|
+
for (const [k, v] of Object.entries(clean)) {
|
|
815
|
+
if (this.columns.has(k)) colValues[k] = v;
|
|
816
|
+
else overflow[k] = v;
|
|
817
|
+
}
|
|
818
|
+
const cols = [
|
|
819
|
+
"_id",
|
|
820
|
+
"created_at",
|
|
821
|
+
"updated_at",
|
|
822
|
+
"data",
|
|
823
|
+
...this.columnOrder
|
|
824
|
+
];
|
|
825
|
+
const values = [
|
|
826
|
+
id,
|
|
827
|
+
createdAt,
|
|
828
|
+
ts,
|
|
829
|
+
JSON.stringify(overflow),
|
|
830
|
+
...this.columnOrder.map(
|
|
831
|
+
(c) => c in colValues ? this.encodeColumn(c, colValues[c]) : null
|
|
832
|
+
)
|
|
833
|
+
];
|
|
834
|
+
const colList = cols.map((c) => `"${c}"`).join(", ");
|
|
835
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
836
|
+
const updateSet = cols.filter((c) => c !== "_id" && c !== "created_at").map((c) => `"${c}" = excluded."${c}"`).join(", ");
|
|
837
|
+
this.db.prepare(
|
|
838
|
+
`INSERT INTO "${this.name}" (${colList}) VALUES (${placeholders}) ON CONFLICT(_id) DO UPDATE SET ${updateSet}`
|
|
839
|
+
).run(...values);
|
|
771
840
|
}
|
|
772
841
|
/* ----------------------------- create ----------------------------- */
|
|
773
842
|
async create(args) {
|
|
@@ -996,7 +1065,12 @@ var Collection = class {
|
|
|
996
1065
|
this.ensureTable();
|
|
997
1066
|
return this.guard(
|
|
998
1067
|
() => aggregate(
|
|
999
|
-
{
|
|
1068
|
+
{
|
|
1069
|
+
db: this.db,
|
|
1070
|
+
table: this.name,
|
|
1071
|
+
onPath: this.trackPath,
|
|
1072
|
+
columns: this.columns
|
|
1073
|
+
},
|
|
1000
1074
|
args
|
|
1001
1075
|
)
|
|
1002
1076
|
);
|
|
@@ -1005,7 +1079,12 @@ var Collection = class {
|
|
|
1005
1079
|
this.ensureTable();
|
|
1006
1080
|
return this.guard(
|
|
1007
1081
|
() => groupBy(
|
|
1008
|
-
{
|
|
1082
|
+
{
|
|
1083
|
+
db: this.db,
|
|
1084
|
+
table: this.name,
|
|
1085
|
+
onPath: this.trackPath,
|
|
1086
|
+
columns: this.columns
|
|
1087
|
+
},
|
|
1009
1088
|
args
|
|
1010
1089
|
)
|
|
1011
1090
|
);
|
|
@@ -1253,12 +1332,14 @@ function stripSystem2(obj) {
|
|
|
1253
1332
|
return rest;
|
|
1254
1333
|
}
|
|
1255
1334
|
var SyncStore = class {
|
|
1256
|
-
constructor(db, nodeId) {
|
|
1335
|
+
constructor(db, nodeId, mon) {
|
|
1257
1336
|
this.db = db;
|
|
1337
|
+
this.mon = mon;
|
|
1258
1338
|
this.init();
|
|
1259
1339
|
this.nodeId = this.resolveNodeId(nodeId);
|
|
1260
1340
|
}
|
|
1261
1341
|
db;
|
|
1342
|
+
mon;
|
|
1262
1343
|
nodeId;
|
|
1263
1344
|
versionSeq = 0;
|
|
1264
1345
|
init() {
|
|
@@ -1293,7 +1374,9 @@ var SyncStore = class {
|
|
|
1293
1374
|
}
|
|
1294
1375
|
resolveNodeId(explicit) {
|
|
1295
1376
|
if (explicit) {
|
|
1296
|
-
this.db.prepare(
|
|
1377
|
+
this.db.prepare(
|
|
1378
|
+
`INSERT OR REPLACE INTO _monlite_meta (key, value) VALUES ('nodeId', ?)`
|
|
1379
|
+
).run(explicit);
|
|
1297
1380
|
return explicit;
|
|
1298
1381
|
}
|
|
1299
1382
|
const row = this.db.prepare(`SELECT value FROM _monlite_meta WHERE key = 'nodeId'`).get();
|
|
@@ -1408,6 +1491,9 @@ var SyncStore = class {
|
|
|
1408
1491
|
);
|
|
1409
1492
|
}
|
|
1410
1493
|
if (winner !== "remote") {
|
|
1494
|
+
if (localVersion !== null) {
|
|
1495
|
+
this.recordLocal(change.collection, change._id, "upsert", Date.now());
|
|
1496
|
+
}
|
|
1411
1497
|
return { applied: false, conflict: localVersion !== null, winner };
|
|
1412
1498
|
}
|
|
1413
1499
|
this.applyData(change);
|
|
@@ -1421,24 +1507,31 @@ var SyncStore = class {
|
|
|
1421
1507
|
change.version,
|
|
1422
1508
|
versionTs(change.version)
|
|
1423
1509
|
);
|
|
1424
|
-
return {
|
|
1510
|
+
return {
|
|
1511
|
+
applied: true,
|
|
1512
|
+
conflict: localVersion !== null,
|
|
1513
|
+
winner: "remote"
|
|
1514
|
+
};
|
|
1425
1515
|
});
|
|
1426
1516
|
}
|
|
1427
1517
|
applyData(change) {
|
|
1428
|
-
const
|
|
1518
|
+
const ts = versionTs(change.version);
|
|
1519
|
+
if (this.mon) {
|
|
1520
|
+
this.mon.collection(change.collection).applyRemoteWrite(change.op, change._id, change.doc, ts);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const coll = change.collection;
|
|
1429
1524
|
this.ensureCollTable(coll);
|
|
1430
|
-
if (op === "delete") {
|
|
1431
|
-
this.db.prepare(`DELETE FROM "${coll}" WHERE _id = ?`).run(_id);
|
|
1525
|
+
if (change.op === "delete") {
|
|
1526
|
+
this.db.prepare(`DELETE FROM "${coll}" WHERE _id = ?`).run(change._id);
|
|
1432
1527
|
return;
|
|
1433
1528
|
}
|
|
1434
1529
|
const doc = change.doc ?? {};
|
|
1435
|
-
const data = JSON.stringify(stripSystem2(doc));
|
|
1436
|
-
const ts = versionTs(change.version);
|
|
1437
1530
|
const createdAt = typeof doc.created_at === "number" ? doc.created_at : ts;
|
|
1438
1531
|
this.db.prepare(
|
|
1439
1532
|
`INSERT INTO "${coll}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)
|
|
1440
1533
|
ON CONFLICT(_id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
|
1441
|
-
).run(_id,
|
|
1534
|
+
).run(change._id, JSON.stringify(stripSystem2(doc)), createdAt, ts);
|
|
1442
1535
|
}
|
|
1443
1536
|
/**
|
|
1444
1537
|
* Latest change per document with `seq` greater than the given watermark,
|
|
@@ -1495,6 +1588,7 @@ var SyncStore = class {
|
|
|
1495
1588
|
this.db.transaction(() => {
|
|
1496
1589
|
for (const coll of collections) {
|
|
1497
1590
|
assertName(coll);
|
|
1591
|
+
if (!this.tableExists(coll)) continue;
|
|
1498
1592
|
const docs = this.db.prepare(`SELECT _id, updated_at FROM "${coll}"`).all();
|
|
1499
1593
|
for (const d of docs) {
|
|
1500
1594
|
if (this.currentVersion(coll, d._id) !== null) continue;
|
|
@@ -1540,7 +1634,14 @@ var SyncStore = class {
|
|
|
1540
1634
|
this.db.prepare(
|
|
1541
1635
|
`INSERT INTO _monlite_conflicts (coll, doc_id, local_version, remote_version, winner, ts)
|
|
1542
1636
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1543
|
-
).run(
|
|
1637
|
+
).run(
|
|
1638
|
+
coll,
|
|
1639
|
+
id,
|
|
1640
|
+
localVersion,
|
|
1641
|
+
remoteVersion,
|
|
1642
|
+
winner,
|
|
1643
|
+
versionTs(remoteVersion)
|
|
1644
|
+
);
|
|
1544
1645
|
}
|
|
1545
1646
|
conflicts() {
|
|
1546
1647
|
const rows = this.db.prepare(
|
|
@@ -1559,6 +1660,8 @@ var SyncStore = class {
|
|
|
1559
1660
|
/* ------------------------------ helpers ------------------------------- */
|
|
1560
1661
|
readDoc(coll, id) {
|
|
1561
1662
|
assertName(coll);
|
|
1663
|
+
if (this.mon) return this.mon.collection(coll).getRaw(id);
|
|
1664
|
+
if (!this.tableExists(coll)) return null;
|
|
1562
1665
|
const row = this.db.prepare(
|
|
1563
1666
|
`SELECT _id, data, created_at, updated_at FROM "${coll}" WHERE _id = ?`
|
|
1564
1667
|
).get(id);
|
|
@@ -1569,6 +1672,9 @@ var SyncStore = class {
|
|
|
1569
1672
|
doc.updated_at = row.updated_at;
|
|
1570
1673
|
return doc;
|
|
1571
1674
|
}
|
|
1675
|
+
tableExists(name) {
|
|
1676
|
+
return this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?`).get(name) != null;
|
|
1677
|
+
}
|
|
1572
1678
|
ensureCollTable(coll) {
|
|
1573
1679
|
assertName(coll);
|
|
1574
1680
|
this.db.exec(
|
|
@@ -1625,7 +1731,7 @@ var Monlite = class {
|
|
|
1625
1731
|
options.autoIndexAfter ?? 10
|
|
1626
1732
|
);
|
|
1627
1733
|
if (options.sync) {
|
|
1628
|
-
this.$sync = new SyncStore(this.driver, options.nodeId);
|
|
1734
|
+
this.$sync = new SyncStore(this.driver, options.nodeId, this);
|
|
1629
1735
|
}
|
|
1630
1736
|
}
|
|
1631
1737
|
/** Stable node id for LWW tie-breaking (only when sync is enabled). */
|