@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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import Database from 'better-sqlite3';
2
1
  import { randomBytes } from 'crypto';
2
+ import { createRequire } from 'module';
3
3
 
4
- // src/db.ts
4
+ // src/id.ts
5
5
  var PROCESS_UNIQUE = randomBytes(5);
6
6
  var counter = randomBytes(3).readUIntBE(0, 3);
7
7
  function objectId() {
@@ -376,6 +376,44 @@ function aggregate(ctx, args) {
376
376
  }
377
377
  return result;
378
378
  }
379
+ var HAVING_FNS = [
380
+ ["_sum", "SUM"],
381
+ ["_avg", "AVG"],
382
+ ["_min", "MIN"],
383
+ ["_max", "MAX"]
384
+ ];
385
+ function comparisonSql(expr, cmp2, params) {
386
+ const out = [];
387
+ const ops = [
388
+ ["equals", "="],
389
+ ["not", "<>"],
390
+ ["gt", ">"],
391
+ ["gte", ">="],
392
+ ["lt", "<"],
393
+ ["lte", "<="]
394
+ ];
395
+ for (const [key, op] of ops) {
396
+ const v = cmp2[key];
397
+ if (v === void 0) continue;
398
+ params.push(v);
399
+ out.push(`${expr} ${op} ?`);
400
+ }
401
+ return out;
402
+ }
403
+ function buildHaving(having, params) {
404
+ const parts = [];
405
+ if (having._count) {
406
+ parts.push(...comparisonSql("COUNT(*)", having._count, params));
407
+ }
408
+ for (const [kind, fn] of HAVING_FNS) {
409
+ const selection = having[kind];
410
+ if (!selection) continue;
411
+ for (const field of Object.keys(selection)) {
412
+ parts.push(...comparisonSql(`${fn}(${fieldExpr(field)})`, selection[field], params));
413
+ }
414
+ }
415
+ return parts.join(" AND ");
416
+ }
379
417
  function groupBy(ctx, args) {
380
418
  if (!Array.isArray(args.by) || args.by.length === 0) {
381
419
  throw new Error("groupBy requires a non-empty `by` array");
@@ -394,6 +432,10 @@ function groupBy(ctx, args) {
394
432
  const { selects: accSelects, cols } = buildAccumulators(args, ctx.onPath);
395
433
  selects.push(...accSelects);
396
434
  let sql = `SELECT ${selects.join(", ")} FROM "${ctx.table}" WHERE ${where} GROUP BY ${groupExprs.join(", ")}`;
435
+ if (args.having) {
436
+ const havingSql = buildHaving(args.having, params);
437
+ if (havingSql) sql += ` HAVING ${havingSql}`;
438
+ }
397
439
  if (args.orderBy) {
398
440
  const parts = [];
399
441
  for (const key of Object.keys(args.orderBy)) {
@@ -439,7 +481,7 @@ var Collection = class {
439
481
  initialized = false;
440
482
  trackPath = (path) => this.mon.autoIndexer.track(this.name, path);
441
483
  get db() {
442
- return this.mon.sqlite;
484
+ return this.mon.driver;
443
485
  }
444
486
  ensureTable() {
445
487
  if (this.initialized) return;
@@ -485,13 +527,12 @@ var Collection = class {
485
527
  const stmt = this.db.prepare(
486
528
  `INSERT INTO "${this.name}" (_id, data, created_at, updated_at) VALUES (?, ?, ?, ?)`
487
529
  );
488
- const insertAll = this.db.transaction((items) => {
489
- for (const item of items) {
530
+ this.db.transaction(() => {
531
+ for (const item of args.data) {
490
532
  const row = this.prepareInsert(item);
491
533
  stmt.run(row._id, row.data, row.created_at, row.updated_at);
492
534
  }
493
535
  });
494
- insertAll(args.data);
495
536
  return { count: args.data.length };
496
537
  }
497
538
  /* ------------------------------ read ------------------------------ */
@@ -531,6 +572,24 @@ var Collection = class {
531
572
  const row = this.db.prepare(`SELECT COUNT(*) AS n FROM "${this.name}" WHERE ${where}`).get(...params);
532
573
  return row.n;
533
574
  }
575
+ /**
576
+ * Return the distinct values of a field across the collection. Array fields
577
+ * are unwound (each element counts as a value), matching MongoDB's `distinct`.
578
+ */
579
+ async distinct(field, where) {
580
+ this.ensureTable();
581
+ const params = [];
582
+ const clause = buildWhere(where, { params, onPath: this.trackPath });
583
+ let sql;
584
+ if (isReserved(field)) {
585
+ sql = `SELECT DISTINCT ${fieldExpr(field)} AS v FROM "${this.name}" WHERE ${clause} ORDER BY v`;
586
+ } else {
587
+ this.trackPath(field);
588
+ 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`;
589
+ }
590
+ const rows = this.db.prepare(sql).all(...params);
591
+ return rows.map((r) => r.v);
592
+ }
534
593
  /* ----------------------------- update ----------------------------- */
535
594
  runUpdate(where, data, single) {
536
595
  this.ensureTable();
@@ -544,7 +603,7 @@ var Collection = class {
544
603
  const stmt = this.db.prepare(
545
604
  `UPDATE "${this.name}" SET data = ?, updated_at = ? WHERE _id = ?`
546
605
  );
547
- const txn = this.db.transaction(() => {
606
+ return this.db.transaction(() => {
548
607
  const out = [];
549
608
  for (const row of rows) {
550
609
  const current = JSON.parse(row.data);
@@ -559,7 +618,6 @@ var Collection = class {
559
618
  }
560
619
  return out;
561
620
  });
562
- return txn();
563
621
  }
564
622
  async update(args) {
565
623
  return this.runUpdate(args.where, args.data, true)[0] ?? null;
@@ -589,10 +647,9 @@ var Collection = class {
589
647
  const rows = this.db.prepare(selectSql).all(...params);
590
648
  if (!rows.length) return [];
591
649
  const stmt = this.db.prepare(`DELETE FROM "${this.name}" WHERE _id = ?`);
592
- const txn = this.db.transaction(() => {
650
+ this.db.transaction(() => {
593
651
  for (const row of rows) stmt.run(row._id);
594
652
  });
595
- txn();
596
653
  return rows.map((r) => this.rowToDoc(r));
597
654
  }
598
655
  async delete(args) {
@@ -670,6 +727,134 @@ var AutoIndexer = class {
670
727
  }
671
728
  };
672
729
 
730
+ // src/driver/better-sqlite3.ts
731
+ var BetterSqlite3Driver = class {
732
+ name = "better-sqlite3";
733
+ raw;
734
+ verbose;
735
+ constructor(BetterSqlite3, filename, options) {
736
+ this.verbose = options.verbose;
737
+ this.raw = new BetterSqlite3(filename, {
738
+ readonly: options.readonly ?? false
739
+ });
740
+ if (!options.readonly && (options.wal ?? true)) {
741
+ this.raw.pragma("journal_mode = WAL");
742
+ }
743
+ }
744
+ exec(sql) {
745
+ this.verbose?.(sql);
746
+ this.raw.exec(sql);
747
+ }
748
+ prepare(sql) {
749
+ this.verbose?.(sql);
750
+ return this.raw.prepare(sql);
751
+ }
752
+ transaction(fn) {
753
+ return this.raw.transaction(fn)();
754
+ }
755
+ close() {
756
+ this.raw.close();
757
+ }
758
+ };
759
+
760
+ // src/driver/node-sqlite.ts
761
+ var NodeSqliteDriver = class {
762
+ name = "node:sqlite";
763
+ raw;
764
+ verbose;
765
+ depth = 0;
766
+ constructor(nodeSqlite, filename, options) {
767
+ this.verbose = options.verbose;
768
+ const { DatabaseSync } = nodeSqlite;
769
+ this.raw = new DatabaseSync(filename, {
770
+ readOnly: options.readonly ?? false
771
+ });
772
+ if (!options.readonly && (options.wal ?? true)) {
773
+ this.raw.exec("PRAGMA journal_mode = WAL");
774
+ }
775
+ }
776
+ exec(sql) {
777
+ this.verbose?.(sql);
778
+ this.raw.exec(sql);
779
+ }
780
+ prepare(sql) {
781
+ this.verbose?.(sql);
782
+ const stmt = this.raw.prepare(sql);
783
+ return {
784
+ run: (...p) => stmt.run(...p),
785
+ get: (...p) => stmt.get(...p),
786
+ all: (...p) => stmt.all(...p)
787
+ };
788
+ }
789
+ transaction(fn) {
790
+ const savepoint = `monlite_sp_${this.depth}`;
791
+ if (this.depth === 0) this.raw.exec("BEGIN");
792
+ else this.raw.exec(`SAVEPOINT ${savepoint}`);
793
+ this.depth++;
794
+ try {
795
+ const result = fn();
796
+ this.depth--;
797
+ if (this.depth === 0) this.raw.exec("COMMIT");
798
+ else this.raw.exec(`RELEASE ${savepoint}`);
799
+ return result;
800
+ } catch (err) {
801
+ this.depth--;
802
+ if (this.depth === 0) this.raw.exec("ROLLBACK");
803
+ else this.raw.exec(`ROLLBACK TO ${savepoint}; RELEASE ${savepoint}`);
804
+ throw err;
805
+ }
806
+ }
807
+ close() {
808
+ this.raw.close();
809
+ }
810
+ };
811
+
812
+ // src/driver/index.ts
813
+ var req = createRequire(import.meta.url);
814
+ function loadBetterSqlite3() {
815
+ try {
816
+ const mod = req("better-sqlite3");
817
+ return mod?.default ?? mod;
818
+ } catch {
819
+ return null;
820
+ }
821
+ }
822
+ function loadNodeSqlite() {
823
+ try {
824
+ return req("node:sqlite");
825
+ } catch {
826
+ return null;
827
+ }
828
+ }
829
+ function createDriver(filename, options = {}) {
830
+ const choice = options.driver ?? "auto";
831
+ if (choice === "better-sqlite3") {
832
+ const mod = loadBetterSqlite3();
833
+ if (!mod) {
834
+ throw new MonliteError(
835
+ `driver "better-sqlite3" was requested but the package is not installed. Run \`npm install better-sqlite3\`.`
836
+ );
837
+ }
838
+ return new BetterSqlite3Driver(mod, filename, options);
839
+ }
840
+ if (choice === "node:sqlite") {
841
+ const mod = loadNodeSqlite();
842
+ if (!mod) {
843
+ throw new MonliteError(
844
+ `driver "node:sqlite" is unavailable. It requires Node >= 22.5.`
845
+ );
846
+ }
847
+ return new NodeSqliteDriver(mod, filename, options);
848
+ }
849
+ const better = loadBetterSqlite3();
850
+ if (better) return new BetterSqlite3Driver(better, filename, options);
851
+ const node = loadNodeSqlite();
852
+ if (node) return new NodeSqliteDriver(node, filename, options);
853
+ throw new MonliteError(
854
+ `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.`
855
+ );
856
+ }
857
+
673
858
  // src/db.ts
674
859
  function validateName(name) {
675
860
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
@@ -691,27 +876,33 @@ function buildTagged(strings, values) {
691
876
  return { sql, params };
692
877
  }
693
878
  var Monlite = class {
694
- /** The underlying better-sqlite3 connection (escape hatch). */
695
- sqlite;
879
+ /** @internal The active SQLite driver. */
880
+ driver;
696
881
  /** @internal */
697
882
  autoIndexer;
698
883
  collections = /* @__PURE__ */ new Map();
699
884
  closed = false;
700
885
  constructor(filename, options = {}) {
701
- const verbose = options.verbose;
702
- this.sqlite = new Database(filename, {
703
- readonly: options.readonly ?? false,
704
- ...verbose ? { verbose: (msg) => verbose(String(msg)) } : {}
886
+ this.driver = createDriver(filename, {
887
+ driver: options.driver,
888
+ readonly: options.readonly,
889
+ wal: options.wal,
890
+ verbose: options.verbose
705
891
  });
706
- if (!options.readonly && (options.wal ?? true)) {
707
- this.sqlite.pragma("journal_mode = WAL");
708
- }
709
892
  this.autoIndexer = new AutoIndexer(
710
- this.sqlite,
893
+ this.driver,
711
894
  options.autoIndex ?? true,
712
895
  options.autoIndexAfter ?? 10
713
896
  );
714
897
  }
898
+ /** The underlying native database handle (escape hatch). */
899
+ get sqlite() {
900
+ return this.driver.raw;
901
+ }
902
+ /** Name of the active backend: `"better-sqlite3"` or `"node:sqlite"`. */
903
+ get driverName() {
904
+ return this.driver.name;
905
+ }
715
906
  /** Get (or lazily create) a typed collection handle. */
716
907
  collection(name) {
717
908
  this.assertOpen();
@@ -727,26 +918,26 @@ var Monlite = class {
727
918
  $queryRaw(strings, ...values) {
728
919
  this.assertOpen();
729
920
  const { sql, params } = buildTagged(strings, values);
730
- return Promise.resolve(this.sqlite.prepare(sql).all(...params));
921
+ return Promise.resolve(this.driver.prepare(sql).all(...params));
731
922
  }
732
923
  /** Like {@link $queryRaw} but takes a raw SQL string and positional params. */
733
924
  $queryRawUnsafe(sql, ...params) {
734
925
  this.assertOpen();
735
926
  return Promise.resolve(
736
- this.sqlite.prepare(sql).all(...params.map(bindable))
927
+ this.driver.prepare(sql).all(...params.map(bindable))
737
928
  );
738
929
  }
739
930
  /** Tagged-template SQL statement returning the number of affected rows. */
740
931
  $executeRaw(strings, ...values) {
741
932
  this.assertOpen();
742
933
  const { sql, params } = buildTagged(strings, values);
743
- return Promise.resolve(this.sqlite.prepare(sql).run(...params).changes);
934
+ return Promise.resolve(this.driver.prepare(sql).run(...params).changes);
744
935
  }
745
936
  /** Like {@link $executeRaw} but takes a raw SQL string and positional params. */
746
937
  $executeRawUnsafe(sql, ...params) {
747
938
  this.assertOpen();
748
939
  return Promise.resolve(
749
- this.sqlite.prepare(sql).run(...params.map(bindable)).changes
940
+ this.driver.prepare(sql).run(...params.map(bindable)).changes
750
941
  );
751
942
  }
752
943
  /**
@@ -755,13 +946,12 @@ var Monlite = class {
755
946
  */
756
947
  async $transaction(fn) {
757
948
  this.assertOpen();
758
- const txn = this.sqlite.transaction(() => fn(this));
759
- return txn();
949
+ return this.driver.transaction(() => fn(this));
760
950
  }
761
951
  /** List all collection (table) names. */
762
952
  $collections() {
763
953
  this.assertOpen();
764
- const rows = this.sqlite.prepare(
954
+ const rows = this.driver.prepare(
765
955
  `SELECT name FROM sqlite_master
766
956
  WHERE type='table' AND name NOT LIKE 'sqlite_%'
767
957
  ORDER BY name`
@@ -772,7 +962,7 @@ var Monlite = class {
772
962
  $drop(name) {
773
963
  this.assertOpen();
774
964
  validateName(name);
775
- this.sqlite.exec(`DROP TABLE IF EXISTS "${name}"`);
965
+ this.driver.exec(`DROP TABLE IF EXISTS "${name}"`);
776
966
  this.collections.delete(name);
777
967
  this.autoIndexer.reset(name);
778
968
  return Promise.resolve();
@@ -785,7 +975,7 @@ var Monlite = class {
785
975
  $disconnect() {
786
976
  if (!this.closed) {
787
977
  this.closed = true;
788
- this.sqlite.close();
978
+ this.driver.close();
789
979
  }
790
980
  return Promise.resolve();
791
981
  }