@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/dist/index.d.cts CHANGED
@@ -27,6 +27,24 @@ declare class Collection<T = Doc> {
27
27
  private columnDdl;
28
28
  /** Auto-additive migration: add declared columns missing from an existing table. */
29
29
  private migrateColumns;
30
+ private foreignKeysOn;
31
+ private declaredIndexDdl;
32
+ /**
33
+ * Reconcile the physical table to the declared schema, performing the changes
34
+ * the auto-additive path can't: **dropping** columns, **renaming** them, and
35
+ * **changing a column's type/constraints** — via a safe, transactional table
36
+ * rebuild that preserves data. Structured collections only.
37
+ *
38
+ * Pass `rename` to map an existing physical column to a new declared name, and
39
+ * `drop` to acknowledge columns that the new schema removes (an unacknowledged
40
+ * column drop throws, so data is never lost by accident).
41
+ *
42
+ * ```ts
43
+ * const users = db.collection("users", { schema: { name: "TEXT", age: "INTEGER" } });
44
+ * await users.$migrate({ rename: { fullname: "name" }, drop: ["legacy"] });
45
+ * ```
46
+ */
47
+ $migrate(options?: MigrateOptions): Promise<void>;
30
48
  private rowToDoc;
31
49
  private encodeColumn;
32
50
  private insertColumns;
@@ -122,6 +140,48 @@ interface MonlitePlugin {
122
140
  collectionMethods?: Record<string, (collection: Collection<any>, ...args: any[]) => any>;
123
141
  }
124
142
 
143
+ /**
144
+ * The minimal SQLite driver surface monlite needs. Implemented by both the
145
+ * better-sqlite3 and the built-in node:sqlite backends so the rest of the
146
+ * codebase is engine-agnostic.
147
+ */
148
+ interface RunResult {
149
+ changes: number;
150
+ lastInsertRowid: number | bigint;
151
+ }
152
+ interface PreparedStatement {
153
+ run(...params: any[]): RunResult;
154
+ get(...params: any[]): any;
155
+ all(...params: any[]): any[];
156
+ }
157
+ interface Driver {
158
+ /** Backend identifier, e.g. "better-sqlite3" or "node:sqlite". */
159
+ readonly name: string;
160
+ exec(sql: string): void;
161
+ prepare(sql: string): PreparedStatement;
162
+ /** Run `fn` inside a transaction; rolls back and rethrows if it throws. */
163
+ transaction<T>(fn: () => T): T;
164
+ close(): void;
165
+ /** Rotate the encryption key (encrypted backends only). */
166
+ rekey?(key: string, cipher?: string): void;
167
+ /** The underlying native handle (better-sqlite3 Database / node:sqlite DatabaseSync). */
168
+ readonly raw: any;
169
+ }
170
+ interface DriverOpenOptions {
171
+ readonly?: boolean;
172
+ wal?: boolean;
173
+ /** Milliseconds to wait on a locked database before erroring. Default 5000. */
174
+ busyTimeout?: number;
175
+ /** Allow loading SQLite extensions (needed by `@monlite/vector`). Default false. */
176
+ allowExtensions?: boolean;
177
+ /** Encrypt the database at rest (better-sqlite3-multiple-ciphers only). */
178
+ encryption?: {
179
+ key: string;
180
+ cipher?: string;
181
+ };
182
+ verbose?: (sql: string) => void;
183
+ }
184
+
125
185
  /**
126
186
  * Public type surface for monlite.
127
187
  *
@@ -167,6 +227,13 @@ interface CollectionOptions {
167
227
  schema?: CollectionSchema;
168
228
  }
169
229
  type CollectionMode = "document" | "structured";
230
+ /** Options for {@link Collection.$migrate}. */
231
+ interface MigrateOptions {
232
+ /** Rename existing physical columns: `{ oldName: newDeclaredName }`. */
233
+ rename?: Record<string, string>;
234
+ /** Acknowledge physical columns to drop (not present in the new schema). */
235
+ drop?: string[];
236
+ }
170
237
  /** A column as reported by {@link Monlite.$schema}. */
171
238
  interface ColumnInfo {
172
239
  name: string;
@@ -371,8 +438,10 @@ interface MonliteOptions {
371
438
  /**
372
439
  * Which SQLite backend to use. `"auto"` (default) prefers `better-sqlite3`
373
440
  * when installed, otherwise the built-in `node:sqlite` (Node >= 22.5).
441
+ * You can also pass a custom {@link Driver} instance (e.g. `@monlite/wasm`
442
+ * for the browser).
374
443
  */
375
- driver?: DriverName;
444
+ driver?: DriverName | Driver;
376
445
  /**
377
446
  * Encrypt the database at rest. Requires the `better-sqlite3-multiple-ciphers`
378
447
  * package (a drop-in for `better-sqlite3`); not supported on `node:sqlite`.
@@ -408,34 +477,6 @@ interface MonliteOptions {
408
477
  verbose?: (sql: string) => void;
409
478
  }
410
479
 
411
- /**
412
- * The minimal SQLite driver surface monlite needs. Implemented by both the
413
- * better-sqlite3 and the built-in node:sqlite backends so the rest of the
414
- * codebase is engine-agnostic.
415
- */
416
- interface RunResult {
417
- changes: number;
418
- lastInsertRowid: number | bigint;
419
- }
420
- interface PreparedStatement {
421
- run(...params: any[]): RunResult;
422
- get(...params: any[]): any;
423
- all(...params: any[]): any[];
424
- }
425
- interface Driver {
426
- /** Backend identifier, e.g. "better-sqlite3" or "node:sqlite". */
427
- readonly name: string;
428
- exec(sql: string): void;
429
- prepare(sql: string): PreparedStatement;
430
- /** Run `fn` inside a transaction; rolls back and rethrows if it throws. */
431
- transaction<T>(fn: () => T): T;
432
- close(): void;
433
- /** Rotate the encryption key (encrypted backends only). */
434
- rekey?(key: string, cipher?: string): void;
435
- /** The underlying native handle (better-sqlite3 Database / node:sqlite DatabaseSync). */
436
- readonly raw: any;
437
- }
438
-
439
480
  /**
440
481
  * Tracks which JSON paths are queried per collection and silently creates a
441
482
  * SQLite expression index once a path crosses the configured threshold.
@@ -747,4 +788,4 @@ declare function objectId(): string;
747
788
  /** True when a value looks like a monlite/ObjectId id (24 hex chars). */
748
789
  declare function isObjectId(value: unknown): value is string;
749
790
 
750
- export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type EncryptionOptions, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
791
+ export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type DriverOpenOptions, type EncryptionOptions, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, type MigrateOptions, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type RunResult, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
package/dist/index.d.ts CHANGED
@@ -27,6 +27,24 @@ declare class Collection<T = Doc> {
27
27
  private columnDdl;
28
28
  /** Auto-additive migration: add declared columns missing from an existing table. */
29
29
  private migrateColumns;
30
+ private foreignKeysOn;
31
+ private declaredIndexDdl;
32
+ /**
33
+ * Reconcile the physical table to the declared schema, performing the changes
34
+ * the auto-additive path can't: **dropping** columns, **renaming** them, and
35
+ * **changing a column's type/constraints** — via a safe, transactional table
36
+ * rebuild that preserves data. Structured collections only.
37
+ *
38
+ * Pass `rename` to map an existing physical column to a new declared name, and
39
+ * `drop` to acknowledge columns that the new schema removes (an unacknowledged
40
+ * column drop throws, so data is never lost by accident).
41
+ *
42
+ * ```ts
43
+ * const users = db.collection("users", { schema: { name: "TEXT", age: "INTEGER" } });
44
+ * await users.$migrate({ rename: { fullname: "name" }, drop: ["legacy"] });
45
+ * ```
46
+ */
47
+ $migrate(options?: MigrateOptions): Promise<void>;
30
48
  private rowToDoc;
31
49
  private encodeColumn;
32
50
  private insertColumns;
@@ -122,6 +140,48 @@ interface MonlitePlugin {
122
140
  collectionMethods?: Record<string, (collection: Collection<any>, ...args: any[]) => any>;
123
141
  }
124
142
 
143
+ /**
144
+ * The minimal SQLite driver surface monlite needs. Implemented by both the
145
+ * better-sqlite3 and the built-in node:sqlite backends so the rest of the
146
+ * codebase is engine-agnostic.
147
+ */
148
+ interface RunResult {
149
+ changes: number;
150
+ lastInsertRowid: number | bigint;
151
+ }
152
+ interface PreparedStatement {
153
+ run(...params: any[]): RunResult;
154
+ get(...params: any[]): any;
155
+ all(...params: any[]): any[];
156
+ }
157
+ interface Driver {
158
+ /** Backend identifier, e.g. "better-sqlite3" or "node:sqlite". */
159
+ readonly name: string;
160
+ exec(sql: string): void;
161
+ prepare(sql: string): PreparedStatement;
162
+ /** Run `fn` inside a transaction; rolls back and rethrows if it throws. */
163
+ transaction<T>(fn: () => T): T;
164
+ close(): void;
165
+ /** Rotate the encryption key (encrypted backends only). */
166
+ rekey?(key: string, cipher?: string): void;
167
+ /** The underlying native handle (better-sqlite3 Database / node:sqlite DatabaseSync). */
168
+ readonly raw: any;
169
+ }
170
+ interface DriverOpenOptions {
171
+ readonly?: boolean;
172
+ wal?: boolean;
173
+ /** Milliseconds to wait on a locked database before erroring. Default 5000. */
174
+ busyTimeout?: number;
175
+ /** Allow loading SQLite extensions (needed by `@monlite/vector`). Default false. */
176
+ allowExtensions?: boolean;
177
+ /** Encrypt the database at rest (better-sqlite3-multiple-ciphers only). */
178
+ encryption?: {
179
+ key: string;
180
+ cipher?: string;
181
+ };
182
+ verbose?: (sql: string) => void;
183
+ }
184
+
125
185
  /**
126
186
  * Public type surface for monlite.
127
187
  *
@@ -167,6 +227,13 @@ interface CollectionOptions {
167
227
  schema?: CollectionSchema;
168
228
  }
169
229
  type CollectionMode = "document" | "structured";
230
+ /** Options for {@link Collection.$migrate}. */
231
+ interface MigrateOptions {
232
+ /** Rename existing physical columns: `{ oldName: newDeclaredName }`. */
233
+ rename?: Record<string, string>;
234
+ /** Acknowledge physical columns to drop (not present in the new schema). */
235
+ drop?: string[];
236
+ }
170
237
  /** A column as reported by {@link Monlite.$schema}. */
171
238
  interface ColumnInfo {
172
239
  name: string;
@@ -371,8 +438,10 @@ interface MonliteOptions {
371
438
  /**
372
439
  * Which SQLite backend to use. `"auto"` (default) prefers `better-sqlite3`
373
440
  * when installed, otherwise the built-in `node:sqlite` (Node >= 22.5).
441
+ * You can also pass a custom {@link Driver} instance (e.g. `@monlite/wasm`
442
+ * for the browser).
374
443
  */
375
- driver?: DriverName;
444
+ driver?: DriverName | Driver;
376
445
  /**
377
446
  * Encrypt the database at rest. Requires the `better-sqlite3-multiple-ciphers`
378
447
  * package (a drop-in for `better-sqlite3`); not supported on `node:sqlite`.
@@ -408,34 +477,6 @@ interface MonliteOptions {
408
477
  verbose?: (sql: string) => void;
409
478
  }
410
479
 
411
- /**
412
- * The minimal SQLite driver surface monlite needs. Implemented by both the
413
- * better-sqlite3 and the built-in node:sqlite backends so the rest of the
414
- * codebase is engine-agnostic.
415
- */
416
- interface RunResult {
417
- changes: number;
418
- lastInsertRowid: number | bigint;
419
- }
420
- interface PreparedStatement {
421
- run(...params: any[]): RunResult;
422
- get(...params: any[]): any;
423
- all(...params: any[]): any[];
424
- }
425
- interface Driver {
426
- /** Backend identifier, e.g. "better-sqlite3" or "node:sqlite". */
427
- readonly name: string;
428
- exec(sql: string): void;
429
- prepare(sql: string): PreparedStatement;
430
- /** Run `fn` inside a transaction; rolls back and rethrows if it throws. */
431
- transaction<T>(fn: () => T): T;
432
- close(): void;
433
- /** Rotate the encryption key (encrypted backends only). */
434
- rekey?(key: string, cipher?: string): void;
435
- /** The underlying native handle (better-sqlite3 Database / node:sqlite DatabaseSync). */
436
- readonly raw: any;
437
- }
438
-
439
480
  /**
440
481
  * Tracks which JSON paths are queried per collection and silently creates a
441
482
  * SQLite expression index once a path crosses the configured threshold.
@@ -747,4 +788,4 @@ declare function objectId(): string;
747
788
  /** True when a value looks like a monlite/ObjectId id (24 hex chars). */
748
789
  declare function isObjectId(value: unknown): value is string;
749
790
 
750
- export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type EncryptionOptions, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
791
+ export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type DriverOpenOptions, type EncryptionOptions, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, type MigrateOptions, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type RunResult, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
package/dist/index.js CHANGED
@@ -790,6 +790,124 @@ var Collection = class {
790
790
  }
791
791
  }
792
792
  }
793
+ foreignKeysOn() {
794
+ try {
795
+ const row = this.db.prepare(`PRAGMA foreign_keys`).get();
796
+ return !!row?.foreign_keys;
797
+ } catch {
798
+ return true;
799
+ }
800
+ }
801
+ declaredIndexDdl() {
802
+ const out = [];
803
+ for (const field of this.columnOrder) {
804
+ if (this.columnDefs[field].index) {
805
+ out.push(
806
+ `CREATE INDEX IF NOT EXISTS "idx_${this.name}_${field}" ON "${this.name}"("${field}")`
807
+ );
808
+ }
809
+ }
810
+ return out;
811
+ }
812
+ /**
813
+ * Reconcile the physical table to the declared schema, performing the changes
814
+ * the auto-additive path can't: **dropping** columns, **renaming** them, and
815
+ * **changing a column's type/constraints** — via a safe, transactional table
816
+ * rebuild that preserves data. Structured collections only.
817
+ *
818
+ * Pass `rename` to map an existing physical column to a new declared name, and
819
+ * `drop` to acknowledge columns that the new schema removes (an unacknowledged
820
+ * column drop throws, so data is never lost by accident).
821
+ *
822
+ * ```ts
823
+ * const users = db.collection("users", { schema: { name: "TEXT", age: "INTEGER" } });
824
+ * await users.$migrate({ rename: { fullname: "name" }, drop: ["legacy"] });
825
+ * ```
826
+ */
827
+ async $migrate(options = {}) {
828
+ if (this.mode !== "structured") {
829
+ throw new MonliteError(
830
+ `$migrate() is only available on structured collections (declare a schema).`
831
+ );
832
+ }
833
+ this.ensureTable();
834
+ const rename = options.rename ?? {};
835
+ const drop = new Set(options.drop ?? []);
836
+ const SYSTEM = /* @__PURE__ */ new Set(["_id", "created_at", "updated_at", "data"]);
837
+ const physical = this.db.prepare(`PRAGMA table_info("${this.name}")`).all().map((r) => r.name).filter((n) => !SYSTEM.has(n));
838
+ const physicalSet = new Set(physical);
839
+ const targetSet = new Set(this.columnOrder);
840
+ const renamedFrom = {};
841
+ for (const [from, to] of Object.entries(rename)) {
842
+ if (!physicalSet.has(from)) {
843
+ throw new MonliteError(
844
+ `Cannot rename "${from}": no such column in "${this.name}".`
845
+ );
846
+ }
847
+ if (!targetSet.has(to)) {
848
+ throw new MonliteError(
849
+ `Cannot rename "${from}" to "${to}": "${to}" is not in the schema.`
850
+ );
851
+ }
852
+ renamedFrom[to] = from;
853
+ }
854
+ const renameSources = new Set(Object.keys(rename));
855
+ for (const col of physical) {
856
+ if (targetSet.has(col) || renameSources.has(col) || drop.has(col))
857
+ continue;
858
+ throw new MonliteError(
859
+ `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.`
860
+ );
861
+ }
862
+ const tmp = `__mon_migrate_${this.name}`;
863
+ const newCols = [
864
+ `_id TEXT PRIMARY KEY`,
865
+ `created_at INTEGER NOT NULL`,
866
+ `updated_at INTEGER NOT NULL`,
867
+ `data TEXT NOT NULL DEFAULT '{}'`,
868
+ ...this.columnOrder.map((f) => this.columnDdl(f, false))
869
+ ];
870
+ const destCols = [
871
+ "_id",
872
+ "created_at",
873
+ "updated_at",
874
+ "data",
875
+ ...this.columnOrder.map((f) => `"${f}"`)
876
+ ].join(", ");
877
+ const srcCols = [
878
+ "_id",
879
+ "created_at",
880
+ "updated_at",
881
+ "data",
882
+ ...this.columnOrder.map((t) => {
883
+ const src = renamedFrom[t] ?? (physicalSet.has(t) ? t : null);
884
+ return src ? `"${src}"` : "NULL";
885
+ })
886
+ ].join(", ");
887
+ const fkOn = this.foreignKeysOn();
888
+ if (fkOn) this.db.exec(`PRAGMA foreign_keys = OFF`);
889
+ try {
890
+ this.guard(
891
+ () => this.db.transaction(() => {
892
+ this.db.exec(`DROP TABLE IF EXISTS "${tmp}"`);
893
+ this.db.exec(
894
+ `CREATE TABLE "${tmp}" (
895
+ ${newCols.join(",\n ")}
896
+ )`
897
+ );
898
+ this.db.exec(
899
+ `INSERT INTO "${tmp}" (${destCols}) SELECT ${srcCols} FROM "${this.name}"`
900
+ );
901
+ this.db.exec(`DROP TABLE "${this.name}"`);
902
+ this.db.exec(`ALTER TABLE "${tmp}" RENAME TO "${this.name}"`);
903
+ for (const ddl of this.declaredIndexDdl()) this.db.exec(ddl);
904
+ })
905
+ );
906
+ } finally {
907
+ if (fkOn) this.db.exec(`PRAGMA foreign_keys = ON`);
908
+ }
909
+ this.insertSqlCache = void 0;
910
+ }
793
911
  /* --------------------------- row <-> doc -------------------------- */
794
912
  rowToDoc(row) {
795
913
  const doc = this.mode === "document" ? JSON.parse(row.data) : JSON.parse(row.data ?? "{}");
@@ -1494,7 +1612,11 @@ function loadNodeSqlite() {
1494
1612
  return null;
1495
1613
  }
1496
1614
  }
1615
+ function isDriverInstance(d) {
1616
+ return typeof d === "object" && d !== null && typeof d.prepare === "function" && typeof d.exec === "function";
1617
+ }
1497
1618
  function createDriver(filename, options = {}) {
1619
+ if (isDriverInstance(options.driver)) return options.driver;
1498
1620
  const choice = options.driver ?? "auto";
1499
1621
  if (options.encryption) {
1500
1622
  if (choice === "node:sqlite") {