@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 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
- > Note: structured collections are not yet covered by `@monlite/sync` (document
383
- > collections are) that's planned follow-up work.
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)) setPath(next, path, value);
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(`${fn}(${fieldExpr(field, columns)})`, selection[field], params)
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(args, ctx.onPath, ctx.columns);
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) line += ` DEFAULT ${formatDefault(def.default)}`;
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 = { ...doc, _id: id, created_at: now, updated_at: now };
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, but only for document collections (structured sync is future work). */
781
+ /** Sync store for recording local changes (both document and structured). */
769
782
  get recorder() {
770
- return this.mode === "document" ? this.mon.$sync : void 0;
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
- { db: this.db, table: this.name, onPath: this.trackPath, columns: this.columns },
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
- { db: this.db, table: this.name, onPath: this.trackPath, columns: this.columns },
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(`INSERT OR REPLACE INTO _monlite_meta (key, value) VALUES ('nodeId', ?)`).run(explicit);
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 { applied: true, conflict: localVersion !== null, winner: "remote" };
1510
+ return {
1511
+ applied: true,
1512
+ conflict: localVersion !== null,
1513
+ winner: "remote"
1514
+ };
1425
1515
  });
1426
1516
  }
1427
1517
  applyData(change) {
1428
- const { collection: coll, _id, op } = change;
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, data, createdAt, ts);
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(coll, id, localVersion, remoteVersion, winner, versionTs(remoteVersion));
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). */