@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 +69 -7
- package/dist/index.cjs +220 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -6
- package/dist/index.d.ts +63 -6
- package/dist/index.js +219 -29
- package/dist/index.js.map +1 -1
- package/package.json +16 -3
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/
|
|
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.
|
|
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
|
-
|
|
489
|
-
for (const item of
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
695
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
readonly: options.readonly
|
|
704
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
978
|
+
this.driver.close();
|
|
789
979
|
}
|
|
790
980
|
return Promise.resolve();
|
|
791
981
|
}
|