@monlite/core 0.1.0 → 0.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 CHANGED
@@ -26,11 +26,18 @@ That's it. No setup. No config. Your data is in `app.db`.
26
26
 
27
27
  ```bash
28
28
  npm install @monlite/core
29
- # or: pnpm add @monlite/core / yarn add @monlite/core
30
29
  ```
31
30
 
32
- monlite uses [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3) under
33
- the hood (its only runtime dependency). Node 18+ is required.
31
+ **monlite has zero required dependencies.** On **Node 22.5+** it uses the
32
+ built-in [`node:sqlite`](https://nodejs.org/api/sqlite.html) engine out of the
33
+ box. To run on Node 18/20 — or to avoid `node:sqlite`'s experimental warning —
34
+ also install the (optional) native driver:
35
+
36
+ ```bash
37
+ npm install @monlite/core better-sqlite3
38
+ ```
39
+
40
+ See [Drivers & zero dependencies](#drivers--zero-dependencies) below.
34
41
 
35
42
  ---
36
43
 
@@ -65,6 +72,7 @@ const mem = createDb(":memory:"); // in-memory database
65
72
 
66
73
  ```ts
67
74
  const db = createDb("./app.db", {
75
+ driver: "auto", // "auto" | "better-sqlite3" | "node:sqlite" (default: "auto")
68
76
  autoIndex: true, // auto-create indexes on hot JSON paths (default: true)
69
77
  autoIndexAfter: 10, // create an index after a path is queried N times (default: 10)
70
78
  readonly: false, // open read-only (default: false)
@@ -242,6 +250,29 @@ const grouped = await users.groupBy({
242
250
  orderBy: { _count: "desc" },
243
251
  });
244
252
  // [ { role: "admin", _count: 5, _sum: { age: 140 } }, … ]
253
+
254
+ // groupBy + having (filter groups by an aggregate, like SQL HAVING)
255
+ await users.groupBy({
256
+ by: ["role"],
257
+ _count: true,
258
+ _sum: { age: true },
259
+ having: {
260
+ _count: { gte: 2 }, // keep groups with COUNT(*) >= 2
261
+ _sum: { age: { gt: 50 } }, // and SUM(age) > 50
262
+ },
263
+ });
264
+ // having comparisons: equals, not, gt, gte, lt, lte — on _count and on
265
+ // _sum/_avg/_min/_max of any field.
266
+ ```
267
+
268
+ ### distinct
269
+
270
+ ```ts
271
+ await users.distinct("role"); // ["admin", "editor"]
272
+ await users.distinct("age", { role: "admin" }); // [28, 31]
273
+
274
+ // Array fields are unwound — each element is a value (like MongoDB):
275
+ await users.distinct("tags"); // ["a", "b", "c"]
245
276
  ```
246
277
 
247
278
  ---
@@ -276,7 +307,9 @@ await db.$transaction((tx) => {
276
307
  });
277
308
  ```
278
309
 
279
- Need the raw driver? `db.sqlite` is the underlying `better-sqlite3` instance.
310
+ Need the raw driver? `db.sqlite` is the underlying native handle (a
311
+ `better-sqlite3` `Database` or a `node:sqlite` `DatabaseSync`, depending on the
312
+ active backend), and `db.driverName` tells you which one is in use.
280
313
 
281
314
  ---
282
315
 
@@ -302,11 +335,39 @@ await db.$collections(); // string[] of collection names
302
335
  await db.$drop("users"); // drop a collection and its data
303
336
  await db.$dropAll(); // drop everything
304
337
  await db.$disconnect(); // close the connection
305
- db.sqlite; // the underlying better-sqlite3 instance
338
+ db.sqlite; // the underlying native driver handle
339
+ db.driverName; // "better-sqlite3" | "node:sqlite"
306
340
  ```
307
341
 
308
342
  ---
309
343
 
344
+ ## Drivers & zero dependencies
345
+
346
+ monlite talks to SQLite through a tiny driver adapter, so it runs on two
347
+ interchangeable backends:
348
+
349
+ | Backend | When it's used | Notes |
350
+ |---|---|---|
351
+ | **`node:sqlite`** | Built into Node **22.5+** | **Zero dependencies.** Still flagged experimental by Node, so it prints a one-time `ExperimentalWarning`. |
352
+ | **`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`. |
353
+
354
+ By default (`driver: "auto"`) monlite uses `better-sqlite3` if it's installed,
355
+ otherwise falls back to the built-in `node:sqlite`. Force one explicitly:
356
+
357
+ ```ts
358
+ createDb("./app.db", { driver: "node:sqlite" }); // zero-dep (Node 22.5+)
359
+ createDb("./app.db", { driver: "better-sqlite3" }); // native, no warning
360
+ ```
361
+
362
+ Both backends pass the exact same test suite, so behavior is identical — pick
363
+ based on your Node version and whether you want the extra dependency.
364
+
365
+ > Want truly zero dependencies on Node 22.5+? Just `npm install @monlite/core`
366
+ > and don't install `better-sqlite3`. To silence the experimental warning,
367
+ > either install `better-sqlite3` or run Node with `--no-warnings`.
368
+
369
+ ---
370
+
310
371
  ## How it works
311
372
 
312
373
  Every collection is a single SQLite table:
@@ -325,8 +386,9 @@ and `updated_at` are real columns. SQLite's built-in `json_extract` /
325
386
  `json_each` power all document queries. No columns are added per field, so
326
387
  there is no schema and no migration — ever.
327
388
 
328
- All operations are synchronous under the hood (better-sqlite3 is sync) but are
329
- exposed as `async` (they return Promises) for API consistency and future-proofing.
389
+ All operations are synchronous under the hood (both SQLite backends are sync)
390
+ but are exposed as `async` (they return Promises) for API consistency and
391
+ future-proofing.
330
392
 
331
393
  ### Notes & limitations
332
394
 
package/dist/index.cjs CHANGED
@@ -1,13 +1,10 @@
1
1
  'use strict';
2
2
 
3
- var Database = require('better-sqlite3');
4
3
  var crypto = require('crypto');
4
+ var module$1 = require('module');
5
5
 
6
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
-
8
- var Database__default = /*#__PURE__*/_interopDefault(Database);
9
-
10
- // src/db.ts
6
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
7
+ // src/id.ts
11
8
  var PROCESS_UNIQUE = crypto.randomBytes(5);
12
9
  var counter = crypto.randomBytes(3).readUIntBE(0, 3);
13
10
  function objectId() {
@@ -382,6 +379,44 @@ function aggregate(ctx, args) {
382
379
  }
383
380
  return result;
384
381
  }
382
+ var HAVING_FNS = [
383
+ ["_sum", "SUM"],
384
+ ["_avg", "AVG"],
385
+ ["_min", "MIN"],
386
+ ["_max", "MAX"]
387
+ ];
388
+ function comparisonSql(expr, cmp2, params) {
389
+ const out = [];
390
+ const ops = [
391
+ ["equals", "="],
392
+ ["not", "<>"],
393
+ ["gt", ">"],
394
+ ["gte", ">="],
395
+ ["lt", "<"],
396
+ ["lte", "<="]
397
+ ];
398
+ for (const [key, op] of ops) {
399
+ const v = cmp2[key];
400
+ if (v === void 0) continue;
401
+ params.push(v);
402
+ out.push(`${expr} ${op} ?`);
403
+ }
404
+ return out;
405
+ }
406
+ function buildHaving(having, params) {
407
+ const parts = [];
408
+ if (having._count) {
409
+ parts.push(...comparisonSql("COUNT(*)", having._count, params));
410
+ }
411
+ for (const [kind, fn] of HAVING_FNS) {
412
+ const selection = having[kind];
413
+ if (!selection) continue;
414
+ for (const field of Object.keys(selection)) {
415
+ parts.push(...comparisonSql(`${fn}(${fieldExpr(field)})`, selection[field], params));
416
+ }
417
+ }
418
+ return parts.join(" AND ");
419
+ }
385
420
  function groupBy(ctx, args) {
386
421
  if (!Array.isArray(args.by) || args.by.length === 0) {
387
422
  throw new Error("groupBy requires a non-empty `by` array");
@@ -400,6 +435,10 @@ function groupBy(ctx, args) {
400
435
  const { selects: accSelects, cols } = buildAccumulators(args, ctx.onPath);
401
436
  selects.push(...accSelects);
402
437
  let sql = `SELECT ${selects.join(", ")} FROM "${ctx.table}" WHERE ${where} GROUP BY ${groupExprs.join(", ")}`;
438
+ if (args.having) {
439
+ const havingSql = buildHaving(args.having, params);
440
+ if (havingSql) sql += ` HAVING ${havingSql}`;
441
+ }
403
442
  if (args.orderBy) {
404
443
  const parts = [];
405
444
  for (const key of Object.keys(args.orderBy)) {
@@ -445,7 +484,7 @@ var Collection = class {
445
484
  initialized = false;
446
485
  trackPath = (path) => this.mon.autoIndexer.track(this.name, path);
447
486
  get db() {
448
- return this.mon.sqlite;
487
+ return this.mon.driver;
449
488
  }
450
489
  ensureTable() {
451
490
  if (this.initialized) return;
@@ -491,13 +530,12 @@ var Collection = class {
491
530
  const stmt = this.db.prepare(
492
531
  `INSERT INTO "${this.name}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)`
493
532
  );
494
- const insertAll = this.db.transaction((items) => {
495
- for (const item of items) {
533
+ this.db.transaction(() => {
534
+ for (const item of args.data) {
496
535
  const row = this.prepareInsert(item);
497
536
  stmt.run(row._id, row.data, row.created_at, row.updated_at);
498
537
  }
499
538
  });
500
- insertAll(args.data);
501
539
  return { count: args.data.length };
502
540
  }
503
541
  /* ------------------------------ read ------------------------------ */
@@ -537,6 +575,24 @@ var Collection = class {
537
575
  const row = this.db.prepare(`SELECT COUNT(*) AS n FROM "${this.name}" WHERE ${where}`).get(...params);
538
576
  return row.n;
539
577
  }
578
+ /**
579
+ * Return the distinct values of a field across the collection. Array fields
580
+ * are unwound (each element counts as a value), matching MongoDB's `distinct`.
581
+ */
582
+ async distinct(field, where) {
583
+ this.ensureTable();
584
+ const params = [];
585
+ const clause = buildWhere(where, { params, onPath: this.trackPath });
586
+ let sql;
587
+ if (isReserved(field)) {
588
+ sql = `SELECT DISTINCT ${fieldExpr(field)} AS v FROM "${this.name}" WHERE ${clause} ORDER BY v`;
589
+ } else {
590
+ this.trackPath(field);
591
+ sql = `SELECT DISTINCT je.value AS v FROM "${this.name}" CROSS JOIN json_each("${this.name}".data, ${pathLiteral(field)}) je WHERE ${clause} ORDER BY v`;
592
+ }
593
+ const rows = this.db.prepare(sql).all(...params);
594
+ return rows.map((r) => r.v);
595
+ }
540
596
  /* ----------------------------- update ----------------------------- */
541
597
  runUpdate(where, data, single) {
542
598
  this.ensureTable();
@@ -550,7 +606,7 @@ var Collection = class {
550
606
  const stmt = this.db.prepare(
551
607
  `UPDATE "${this.name}" SET data = ?, updated_at = ? WHERE _id = ?`
552
608
  );
553
- const txn = this.db.transaction(() => {
609
+ return this.db.transaction(() => {
554
610
  const out = [];
555
611
  for (const row of rows) {
556
612
  const current = JSON.parse(row.data);
@@ -565,7 +621,6 @@ var Collection = class {
565
621
  }
566
622
  return out;
567
623
  });
568
- return txn();
569
624
  }
570
625
  async update(args) {
571
626
  return this.runUpdate(args.where, args.data, true)[0] ?? null;
@@ -595,10 +650,9 @@ var Collection = class {
595
650
  const rows = this.db.prepare(selectSql).all(...params);
596
651
  if (!rows.length) return [];
597
652
  const stmt = this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`);
598
- const txn = this.db.transaction(() => {
653
+ this.db.transaction(() => {
599
654
  for (const row of rows) stmt.run(row._id);
600
655
  });
601
- txn();
602
656
  return rows.map((r) => this.rowToDoc(r));
603
657
  }
604
658
  async delete(args) {
@@ -676,6 +730,134 @@ var AutoIndexer = class {
676
730
  }
677
731
  };
678
732
 
733
+ // src/driver/better-sqlite3.ts
734
+ var BetterSqlite3Driver = class {
735
+ name = "better-sqlite3";
736
+ raw;
737
+ verbose;
738
+ constructor(BetterSqlite3, filename, options) {
739
+ this.verbose = options.verbose;
740
+ this.raw = new BetterSqlite3(filename, {
741
+ readonly: options.readonly ?? false
742
+ });
743
+ if (!options.readonly && (options.wal ?? true)) {
744
+ this.raw.pragma("journal_mode = WAL");
745
+ }
746
+ }
747
+ exec(sql) {
748
+ this.verbose?.(sql);
749
+ this.raw.exec(sql);
750
+ }
751
+ prepare(sql) {
752
+ this.verbose?.(sql);
753
+ return this.raw.prepare(sql);
754
+ }
755
+ transaction(fn) {
756
+ return this.raw.transaction(fn)();
757
+ }
758
+ close() {
759
+ this.raw.close();
760
+ }
761
+ };
762
+
763
+ // src/driver/node-sqlite.ts
764
+ var NodeSqliteDriver = class {
765
+ name = "node:sqlite";
766
+ raw;
767
+ verbose;
768
+ depth = 0;
769
+ constructor(nodeSqlite, filename, options) {
770
+ this.verbose = options.verbose;
771
+ const { DatabaseSync } = nodeSqlite;
772
+ this.raw = new DatabaseSync(filename, {
773
+ readOnly: options.readonly ?? false
774
+ });
775
+ if (!options.readonly && (options.wal ?? true)) {
776
+ this.raw.exec("PRAGMA journal_mode = WAL");
777
+ }
778
+ }
779
+ exec(sql) {
780
+ this.verbose?.(sql);
781
+ this.raw.exec(sql);
782
+ }
783
+ prepare(sql) {
784
+ this.verbose?.(sql);
785
+ const stmt = this.raw.prepare(sql);
786
+ return {
787
+ run: (...p) => stmt.run(...p),
788
+ get: (...p) => stmt.get(...p),
789
+ all: (...p) => stmt.all(...p)
790
+ };
791
+ }
792
+ transaction(fn) {
793
+ const savepoint = `monlite_sp_${this.depth}`;
794
+ if (this.depth === 0) this.raw.exec("BEGIN");
795
+ else this.raw.exec(`SAVEPOINT ${savepoint}`);
796
+ this.depth++;
797
+ try {
798
+ const result = fn();
799
+ this.depth--;
800
+ if (this.depth === 0) this.raw.exec("COMMIT");
801
+ else this.raw.exec(`RELEASE ${savepoint}`);
802
+ return result;
803
+ } catch (err) {
804
+ this.depth--;
805
+ if (this.depth === 0) this.raw.exec("ROLLBACK");
806
+ else this.raw.exec(`ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`);
807
+ throw err;
808
+ }
809
+ }
810
+ close() {
811
+ this.raw.close();
812
+ }
813
+ };
814
+
815
+ // src/driver/index.ts
816
+ var req = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
817
+ function loadBetterSqlite3() {
818
+ try {
819
+ const mod = req("better-sqlite3");
820
+ return mod?.default ?? mod;
821
+ } catch {
822
+ return null;
823
+ }
824
+ }
825
+ function loadNodeSqlite() {
826
+ try {
827
+ return req("node:sqlite");
828
+ } catch {
829
+ return null;
830
+ }
831
+ }
832
+ function createDriver(filename, options = {}) {
833
+ const choice = options.driver ?? "auto";
834
+ if (choice === "better-sqlite3") {
835
+ const mod = loadBetterSqlite3();
836
+ if (!mod) {
837
+ throw new MonliteError(
838
+ `driver "better-sqlite3" was requested but the package is not installed. Run \`npm install better-sqlite3\`.`
839
+ );
840
+ }
841
+ return new BetterSqlite3Driver(mod, filename, options);
842
+ }
843
+ if (choice === "node:sqlite") {
844
+ const mod = loadNodeSqlite();
845
+ if (!mod) {
846
+ throw new MonliteError(
847
+ `driver "node:sqlite" is unavailable. It requires Node >= 22.5.`
848
+ );
849
+ }
850
+ return new NodeSqliteDriver(mod, filename, options);
851
+ }
852
+ const better = loadBetterSqlite3();
853
+ if (better) return new BetterSqlite3Driver(better, filename, options);
854
+ const node = loadNodeSqlite();
855
+ if (node) return new NodeSqliteDriver(node, filename, options);
856
+ throw new MonliteError(
857
+ `No SQLite driver available. Either install better-sqlite3 (\`npm install better-sqlite3\`) or run on Node >= 22.5 for the built-in node:sqlite backend.`
858
+ );
859
+ }
860
+
679
861
  // src/db.ts
680
862
  function validateName(name) {
681
863
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
@@ -697,27 +879,33 @@ function buildTagged(strings, values) {
697
879
  return { sql, params };
698
880
  }
699
881
  var Monlite = class {
700
- /** The underlying better-sqlite3 connection (escape hatch). */
701
- sqlite;
882
+ /** @internal The active SQLite driver. */
883
+ driver;
702
884
  /** @internal */
703
885
  autoIndexer;
704
886
  collections = /* @__PURE__ */ new Map();
705
887
  closed = false;
706
888
  constructor(filename, options = {}) {
707
- const verbose = options.verbose;
708
- this.sqlite = new Database__default.default(filename, {
709
- readonly: options.readonly ?? false,
710
- ...verbose ? { verbose: (msg) => verbose(String(msg)) } : {}
889
+ this.driver = createDriver(filename, {
890
+ driver: options.driver,
891
+ readonly: options.readonly,
892
+ wal: options.wal,
893
+ verbose: options.verbose
711
894
  });
712
- if (!options.readonly && (options.wal ?? true)) {
713
- this.sqlite.pragma("journal_mode = WAL");
714
- }
715
895
  this.autoIndexer = new AutoIndexer(
716
- this.sqlite,
896
+ this.driver,
717
897
  options.autoIndex ?? true,
718
898
  options.autoIndexAfter ?? 10
719
899
  );
720
900
  }
901
+ /** The underlying native database handle (escape hatch). */
902
+ get sqlite() {
903
+ return this.driver.raw;
904
+ }
905
+ /** Name of the active backend: `"better-sqlite3"` or `"node:sqlite"`. */
906
+ get driverName() {
907
+ return this.driver.name;
908
+ }
721
909
  /** Get (or lazily create) a typed collection handle. */
722
910
  collection(name) {
723
911
  this.assertOpen();
@@ -733,26 +921,26 @@ var Monlite = class {
733
921
  $queryRaw(strings, ...values) {
734
922
  this.assertOpen();
735
923
  const { sql, params } = buildTagged(strings, values);
736
- return Promise.resolve(this.sqlite.prepare(sql).all(...params));
924
+ return Promise.resolve(this.driver.prepare(sql).all(...params));
737
925
  }
738
926
  /** Like {@link $queryRaw} but takes a raw SQL string and positional params. */
739
927
  $queryRawUnsafe(sql, ...params) {
740
928
  this.assertOpen();
741
929
  return Promise.resolve(
742
- this.sqlite.prepare(sql).all(...params.map(bindable))
930
+ this.driver.prepare(sql).all(...params.map(bindable))
743
931
  );
744
932
  }
745
933
  /** Tagged-template SQL statement returning the number of affected rows. */
746
934
  $executeRaw(strings, ...values) {
747
935
  this.assertOpen();
748
936
  const { sql, params } = buildTagged(strings, values);
749
- return Promise.resolve(this.sqlite.prepare(sql).run(...params).changes);
937
+ return Promise.resolve(this.driver.prepare(sql).run(...params).changes);
750
938
  }
751
939
  /** Like {@link $executeRaw} but takes a raw SQL string and positional params. */
752
940
  $executeRawUnsafe(sql, ...params) {
753
941
  this.assertOpen();
754
942
  return Promise.resolve(
755
- this.sqlite.prepare(sql).run(...params.map(bindable)).changes
943
+ this.driver.prepare(sql).run(...params.map(bindable)).changes
756
944
  );
757
945
  }
758
946
  /**
@@ -761,13 +949,12 @@ var Monlite = class {
761
949
  */
762
950
  async $transaction(fn) {
763
951
  this.assertOpen();
764
- const txn = this.sqlite.transaction(() => fn(this));
765
- return txn();
952
+ return this.driver.transaction(() => fn(this));
766
953
  }
767
954
  /** List all collection (table) names. */
768
955
  $collections() {
769
956
  this.assertOpen();
770
- const rows = this.sqlite.prepare(
957
+ const rows = this.driver.prepare(
771
958
  `SELECT name FROM sqlite_master
772
959
  WHERE type='table' AND name NOT LIKE 'sqlite_%'
773
960
  ORDER BY name`
@@ -778,7 +965,7 @@ var Monlite = class {
778
965
  $drop(name) {
779
966
  this.assertOpen();
780
967
  validateName(name);
781
- this.sqlite.exec(`DROP TABLE IF EXISTS "${name}"`);
968
+ this.driver.exec(`DROP TABLE IF EXISTS "${name}"`);
782
969
  this.collections.delete(name);
783
970
  this.autoIndexer.reset(name);
784
971
  return Promise.resolve();
@@ -791,7 +978,7 @@ var Monlite = class {
791
978
  $disconnect() {
792
979
  if (!this.closed) {
793
980
  this.closed = true;
794
- this.sqlite.close();
981
+ this.driver.close();
795
982
  }
796
983
  return Promise.resolve();
797
984
  }