@monlite/core 1.1.0 → 1.3.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 +21 -6
- package/dist/index.cjs +122 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -30
- package/dist/index.d.ts +71 -30
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -389,8 +389,21 @@ definition supports `index`, `unique`, `notNull`, `default`, and `references`.
|
|
|
389
389
|
|
|
390
390
|
**Migrations are automatic for additive changes.** Re-opening a collection with a
|
|
391
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.
|
|
393
|
-
|
|
392
|
+
`NOT NULL` columns a `default` so existing rows can be backfilled.
|
|
393
|
+
|
|
394
|
+
For **destructive changes** — dropping, renaming, or changing a column's
|
|
395
|
+
type/constraints — call `$migrate()`. It safely rebuilds the table (in a
|
|
396
|
+
transaction, preserving data and indexes) to match the new declared schema:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
// v2 schema: `fullname` → `name`, `age` is now INTEGER, `legacy` removed.
|
|
400
|
+
const users = db.collection("users", { schema: { name: "TEXT", age: "INTEGER" } });
|
|
401
|
+
await users.$migrate({ rename: { fullname: "name" }, drop: ["legacy"] });
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
A column that exists on disk but isn't in the new schema (and isn't listed in
|
|
405
|
+
`drop`) throws — so you never lose data by accident. Run migrations at startup,
|
|
406
|
+
before using the collection.
|
|
394
407
|
|
|
395
408
|
### Do I have to care: JSON vs native columns?
|
|
396
409
|
|
|
@@ -446,9 +459,10 @@ await engine.start();
|
|
|
446
459
|
```
|
|
447
460
|
|
|
448
461
|
Pull / push / two-way replication, last-write-wins (or custom) conflict
|
|
449
|
-
resolution, and pluggable adapters (`MongoAdapter`, `
|
|
450
|
-
monlite-to-monlite, `MemoryAdapter` for
|
|
451
|
-
|
|
462
|
+
resolution, and pluggable adapters (`MongoAdapter`, `PostgresAdapter`,
|
|
463
|
+
`MySqlAdapter`, `MonliteAdapter` for monlite-to-monlite, `MemoryAdapter` for
|
|
464
|
+
tests) — keep local monlite as the embedded runtime and a server DB as the cloud
|
|
465
|
+
of record. See the
|
|
452
466
|
[`@monlite/sync` README](https://www.npmjs.com/package/@monlite/sync) for details.
|
|
453
467
|
|
|
454
468
|
---
|
|
@@ -587,13 +601,14 @@ Redis/Mongo/Qdrant replacement. For scale, keep the real services and
|
|
|
587
601
|
|
|
588
602
|
## Drivers & zero dependencies
|
|
589
603
|
|
|
590
|
-
monlite talks to SQLite through a tiny driver adapter, so it runs on
|
|
604
|
+
monlite talks to SQLite through a tiny driver adapter, so it runs on
|
|
591
605
|
interchangeable backends:
|
|
592
606
|
|
|
593
607
|
| Backend | When it's used | Notes |
|
|
594
608
|
|---|---|---|
|
|
595
609
|
| **`node:sqlite`** | Built into Node **22.5+** | **Zero dependencies.** Still flagged experimental by Node, so it prints a one-time `ExperimentalWarning`. |
|
|
596
610
|
| **`better-sqlite3`** | When the package is installed | Battle-tested native driver. Works on Node 18/20/22, no warning. Install it yourself: `npm i better-sqlite3`. |
|
|
611
|
+
| **WASM (browser)** | Via [`@monlite/wasm`](https://www.npmjs.com/package/@monlite/wasm) | Runs monlite **in the browser** on SQLite-WASM (sql.js); pass `driver: wasmDriver(SQL)`. Snapshot persistence to IndexedDB/OPFS. |
|
|
597
612
|
|
|
598
613
|
By default (`driver: "auto"`) monlite uses `better-sqlite3` if it's installed,
|
|
599
614
|
otherwise falls back to the built-in `node:sqlite`. Force one explicitly:
|
package/dist/index.cjs
CHANGED
|
@@ -793,6 +793,124 @@ var Collection = class {
|
|
|
793
793
|
}
|
|
794
794
|
}
|
|
795
795
|
}
|
|
796
|
+
foreignKeysOn() {
|
|
797
|
+
try {
|
|
798
|
+
const row = this.db.prepare(`PRAGMA foreign_keys`).get();
|
|
799
|
+
return !!row?.foreign_keys;
|
|
800
|
+
} catch {
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
declaredIndexDdl() {
|
|
805
|
+
const out = [];
|
|
806
|
+
for (const field of this.columnOrder) {
|
|
807
|
+
if (this.columnDefs[field].index) {
|
|
808
|
+
out.push(
|
|
809
|
+
`CREATE INDEX IF NOT EXISTS "idx_${this.name}_${field}" ON "${this.name}"("${field}")`
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return out;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Reconcile the physical table to the declared schema, performing the changes
|
|
817
|
+
* the auto-additive path can't: **dropping** columns, **renaming** them, and
|
|
818
|
+
* **changing a column's type/constraints** — via a safe, transactional table
|
|
819
|
+
* rebuild that preserves data. Structured collections only.
|
|
820
|
+
*
|
|
821
|
+
* Pass `rename` to map an existing physical column to a new declared name, and
|
|
822
|
+
* `drop` to acknowledge columns that the new schema removes (an unacknowledged
|
|
823
|
+
* column drop throws, so data is never lost by accident).
|
|
824
|
+
*
|
|
825
|
+
* ```ts
|
|
826
|
+
* const users = db.collection("users", { schema: { name: "TEXT", age: "INTEGER" } });
|
|
827
|
+
* await users.$migrate({ rename: { fullname: "name" }, drop: ["legacy"] });
|
|
828
|
+
* ```
|
|
829
|
+
*/
|
|
830
|
+
async $migrate(options = {}) {
|
|
831
|
+
if (this.mode !== "structured") {
|
|
832
|
+
throw new MonliteError(
|
|
833
|
+
`$migrate() is only available on structured collections (declare a schema).`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
this.ensureTable();
|
|
837
|
+
const rename = options.rename ?? {};
|
|
838
|
+
const drop = new Set(options.drop ?? []);
|
|
839
|
+
const SYSTEM = /* @__PURE__ */ new Set(["_id", "created_at", "updated_at", "data"]);
|
|
840
|
+
const physical = this.db.prepare(`PRAGMA table_info("${this.name}")`).all().map((r) => r.name).filter((n) => !SYSTEM.has(n));
|
|
841
|
+
const physicalSet = new Set(physical);
|
|
842
|
+
const targetSet = new Set(this.columnOrder);
|
|
843
|
+
const renamedFrom = {};
|
|
844
|
+
for (const [from, to] of Object.entries(rename)) {
|
|
845
|
+
if (!physicalSet.has(from)) {
|
|
846
|
+
throw new MonliteError(
|
|
847
|
+
`Cannot rename "${from}": no such column in "${this.name}".`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (!targetSet.has(to)) {
|
|
851
|
+
throw new MonliteError(
|
|
852
|
+
`Cannot rename "${from}" to "${to}": "${to}" is not in the schema.`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
renamedFrom[to] = from;
|
|
856
|
+
}
|
|
857
|
+
const renameSources = new Set(Object.keys(rename));
|
|
858
|
+
for (const col of physical) {
|
|
859
|
+
if (targetSet.has(col) || renameSources.has(col) || drop.has(col))
|
|
860
|
+
continue;
|
|
861
|
+
throw new MonliteError(
|
|
862
|
+
`Column "${col}" exists in "${this.name}" but isn't in the schema. Add it to the schema, or list it in \`drop\` to remove it.`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
const tmp = `__mon_migrate_${this.name}`;
|
|
866
|
+
const newCols = [
|
|
867
|
+
`_id TEXT PRIMARY KEY`,
|
|
868
|
+
`created_at INTEGER NOT NULL`,
|
|
869
|
+
`updated_at INTEGER NOT NULL`,
|
|
870
|
+
`data TEXT NOT NULL DEFAULT '{}'`,
|
|
871
|
+
...this.columnOrder.map((f) => this.columnDdl(f, false))
|
|
872
|
+
];
|
|
873
|
+
const destCols = [
|
|
874
|
+
"_id",
|
|
875
|
+
"created_at",
|
|
876
|
+
"updated_at",
|
|
877
|
+
"data",
|
|
878
|
+
...this.columnOrder.map((f) => `"${f}"`)
|
|
879
|
+
].join(", ");
|
|
880
|
+
const srcCols = [
|
|
881
|
+
"_id",
|
|
882
|
+
"created_at",
|
|
883
|
+
"updated_at",
|
|
884
|
+
"data",
|
|
885
|
+
...this.columnOrder.map((t) => {
|
|
886
|
+
const src = renamedFrom[t] ?? (physicalSet.has(t) ? t : null);
|
|
887
|
+
return src ? `"${src}"` : "NULL";
|
|
888
|
+
})
|
|
889
|
+
].join(", ");
|
|
890
|
+
const fkOn = this.foreignKeysOn();
|
|
891
|
+
if (fkOn) this.db.exec(`PRAGMA foreign_keys = OFF`);
|
|
892
|
+
try {
|
|
893
|
+
this.guard(
|
|
894
|
+
() => this.db.transaction(() => {
|
|
895
|
+
this.db.exec(`DROP TABLE IF EXISTS "${tmp}"`);
|
|
896
|
+
this.db.exec(
|
|
897
|
+
`CREATE TABLE "${tmp}" (
|
|
898
|
+
${newCols.join(",\n ")}
|
|
899
|
+
)`
|
|
900
|
+
);
|
|
901
|
+
this.db.exec(
|
|
902
|
+
`INSERT INTO "${tmp}" (${destCols}) SELECT ${srcCols} FROM "${this.name}"`
|
|
903
|
+
);
|
|
904
|
+
this.db.exec(`DROP TABLE "${this.name}"`);
|
|
905
|
+
this.db.exec(`ALTER TABLE "${tmp}" RENAME TO "${this.name}"`);
|
|
906
|
+
for (const ddl of this.declaredIndexDdl()) this.db.exec(ddl);
|
|
907
|
+
})
|
|
908
|
+
);
|
|
909
|
+
} finally {
|
|
910
|
+
if (fkOn) this.db.exec(`PRAGMA foreign_keys = ON`);
|
|
911
|
+
}
|
|
912
|
+
this.insertSqlCache = void 0;
|
|
913
|
+
}
|
|
796
914
|
/* --------------------------- row <-> doc -------------------------- */
|
|
797
915
|
rowToDoc(row) {
|
|
798
916
|
const doc = this.mode === "document" ? JSON.parse(row.data) : JSON.parse(row.data ?? "{}");
|
|
@@ -1497,7 +1615,11 @@ function loadNodeSqlite() {
|
|
|
1497
1615
|
return null;
|
|
1498
1616
|
}
|
|
1499
1617
|
}
|
|
1618
|
+
function isDriverInstance(d) {
|
|
1619
|
+
return typeof d === "object" && d !== null && typeof d.prepare === "function" && typeof d.exec === "function";
|
|
1620
|
+
}
|
|
1500
1621
|
function createDriver(filename, options = {}) {
|
|
1622
|
+
if (isDriverInstance(options.driver)) return options.driver;
|
|
1501
1623
|
const choice = options.driver ?? "auto";
|
|
1502
1624
|
if (options.encryption) {
|
|
1503
1625
|
if (choice === "node:sqlite") {
|