@semics-tech/mongolite 1.1.0 → 1.2.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
@@ -18,12 +18,13 @@ A MongoDB-like client backed by SQLite. Use a familiar MongoDB API with the simp
18
18
  - **MongoDB-compatible API** — `insertOne`, `findOne`, `updateOne`, `deleteOne`, `find`, `aggregate`, and more
19
19
  - **SQLite persistence** — single file, zero configuration, works offline
20
20
  - **Automatic `_id` generation** — UUID assigned on insert if not provided
21
- - **WAL mode** — Write-Ahead Logging for better concurrent read access
21
+ - **WAL mode** — Write-Ahead Logging for better concurrent read access, on by default
22
+ - **Shared connections** — opt into one pooled connection per file per process
22
23
  - **Rich query operators** — `$eq`, `$gt`, `$in`, `$and`, `$or`, `$elemMatch`, `$regex`, and more
23
24
  - **Update operators** — `$set`, `$inc`, `$push`, `$pull`, `$addToSet`, `$mul`, and more
24
25
  - **Indexing** — create, list, and drop indexes including unique and compound indexes
25
26
  - **Change streams** — real-time change tracking via `collection.watch()`
26
- - **JSON safety** — validates documents before insert and recovers from corrupted data
27
+ - **JSON safety** — validates documents before insert, and a corrupted row can't break queries for the rest of the collection
27
28
  - **TypeScript** — fully typed with strict mode
28
29
 
29
30
  ## Installation
@@ -106,6 +107,43 @@ await users.insertOne({ name: 'Alice', age: 30 });
106
107
  await client.close();
107
108
  ```
108
109
 
110
+ ### Connection options
111
+
112
+ ```typescript
113
+ const client = new MongoLite({
114
+ filePath: './myapp.sqlite',
115
+ WAL: true, // Write-Ahead Logging. Default: true
116
+ busyTimeout: 5000, // ms to wait for a lock before SQLITE_BUSY. Default: 5000
117
+ shared: true, // reuse one connection per file in this process. Default: false
118
+ readOnly: false,
119
+ verbose: false,
120
+ });
121
+ ```
122
+
123
+ #### Sharing one connection across a process
124
+
125
+ Applications often end up with several clients pointing at the same database file
126
+ — a module-level client, a background job, a request handler — each opening its
127
+ own connection, and each then hand-rolling an instance cache to avoid the
128
+ resulting lock contention. `shared: true` moves that into the library:
129
+
130
+ ```typescript
131
+ // Both resolve to the same file, so they share a single SQLite connection.
132
+ const a = new MongoLite({ filePath: './app.sqlite', shared: true });
133
+ const b = new MongoLite({ filePath: './app.sqlite', shared: true });
134
+
135
+ await a.close(); // b keeps working — the handle is reference counted
136
+ await b.close(); // last holder out actually closes it
137
+ ```
138
+
139
+ Connections are only shared when the resolved path *and* every setting that
140
+ changes the handle's behaviour match. `:memory:` databases are never shared,
141
+ since two `:memory:` opens are two unrelated databases.
142
+
143
+ This dedupes connections **within a process only**. Separate processes each get
144
+ their own, so cross-process contention is handled by WAL mode and `busyTimeout`
145
+ instead.
146
+
109
147
  ### In-memory (tests / ephemeral)
110
148
 
111
149
  ```typescript
@@ -1,5 +1,7 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import type { IDatabaseAdapter, MongoLiteOptions } from '../db.js';
3
+ /** Exposed for tests and diagnostics. */
4
+ export declare function sharedConnectionCount(): number;
3
5
  /**
4
6
  * Database adapter backed by Node.js's built-in `node:sqlite` module — no native
5
7
  * addon, no prebuilt binary, no `node-gyp`. Requires Node.js 22.5.0+ (the module
@@ -33,6 +35,8 @@ export declare class NodeSqliteAdapter implements IDatabaseAdapter {
33
35
  private readonly verbose;
34
36
  private readonly readOnly;
35
37
  private readonly WAL;
38
+ private readonly busyTimeout;
39
+ private readonly shareKey;
36
40
  private openPromise;
37
41
  /**
38
42
  * @param dbPathOrOptions Path to the SQLite database file (use `':memory:'`
@@ -1,4 +1,12 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
+ import path from 'node:path';
3
+ import { ConnectionRegistry, DEFAULT_BUSY_TIMEOUT_MS, buildConnectionKey, isShareablePath, } from '../utils/connection-registry.js';
4
+ /** Shared `node:sqlite` handles for this process, keyed by connection identity. */
5
+ const sharedConnections = new ConnectionRegistry();
6
+ /** Exposed for tests and diagnostics. */
7
+ export function sharedConnectionCount() {
8
+ return sharedConnections.size;
9
+ }
2
10
  /**
3
11
  * Database adapter backed by Node.js's built-in `node:sqlite` module — no native
4
12
  * addon, no prebuilt binary, no `node-gyp`. Requires Node.js 22.5.0+ (the module
@@ -35,18 +43,34 @@ export class NodeSqliteAdapter {
35
43
  constructor(dbPathOrOptions) {
36
44
  this.db = null;
37
45
  this.openPromise = null;
46
+ let shared;
38
47
  if (typeof dbPathOrOptions === 'string') {
39
48
  this.filePath = dbPathOrOptions;
40
49
  this.verbose = false;
41
50
  this.readOnly = false;
42
- this.WAL = false;
51
+ this.WAL = true;
52
+ this.busyTimeout = DEFAULT_BUSY_TIMEOUT_MS;
53
+ shared = false;
43
54
  }
44
55
  else {
45
56
  this.filePath = dbPathOrOptions.filePath;
46
57
  this.verbose = dbPathOrOptions.verbose || false;
47
58
  this.readOnly = dbPathOrOptions.readOnly || false;
48
- this.WAL = dbPathOrOptions.WAL || true;
49
- }
59
+ this.WAL = dbPathOrOptions.WAL ?? true;
60
+ this.busyTimeout = dbPathOrOptions.busyTimeout ?? DEFAULT_BUSY_TIMEOUT_MS;
61
+ shared = dbPathOrOptions.shared ?? false;
62
+ }
63
+ this.shareKey =
64
+ shared && isShareablePath(this.filePath)
65
+ ? buildConnectionKey({
66
+ backend: 'node:sqlite',
67
+ filePath: path.resolve(this.filePath),
68
+ readOnly: this.readOnly,
69
+ WAL: this.WAL,
70
+ busyTimeout: this.busyTimeout,
71
+ verbose: this.verbose,
72
+ })
73
+ : null;
50
74
  }
51
75
  /**
52
76
  * Opens the database connection if it's not already open.
@@ -60,39 +84,52 @@ export class NodeSqliteAdapter {
60
84
  }
61
85
  this.openPromise = new Promise((resolve, reject) => {
62
86
  try {
63
- this.db = new DatabaseSync(this.filePath, { readOnly: this.readOnly });
64
- if (this.WAL && !this.readOnly) {
65
- this.db.exec('PRAGMA journal_mode = WAL');
66
- }
67
- // Register regexp UDF for $regex operator support.
68
- // WARNING: Patterns are compiled via JavaScript RegExp. User-supplied patterns that are
69
- // not validated for catastrophic backtracking (ReDoS) could block the event loop.
70
- // Avoid using untrusted/unvalidated patterns with $regex in security-sensitive contexts.
71
- this.db.function('regexp', { deterministic: true }, (pattern, value) => {
72
- if (typeof pattern !== 'string' || value === null || value === undefined)
73
- return 0;
74
- try {
75
- return new RegExp(pattern).test(String(value)) ? 1 : 0;
76
- }
77
- catch {
78
- return 0;
87
+ // Only runs on a genuinely new connection — when sharing, an existing
88
+ // handle already has its pragmas and UDFs applied.
89
+ const openConnection = () => {
90
+ const db = new DatabaseSync(this.filePath, { readOnly: this.readOnly });
91
+ if (this.WAL && !this.readOnly) {
92
+ db.exec('PRAGMA journal_mode = WAL');
79
93
  }
80
- });
81
- // Register regexp_flags UDF for $regex with $options support
82
- this.db.function('regexp_flags', { deterministic: true }, (pattern, flags, value) => {
83
- if (typeof pattern !== 'string' || value === null || value === undefined)
84
- return 0;
85
- try {
86
- const f = typeof flags === 'string' ? flags : '';
87
- return new RegExp(pattern, f).test(String(value)) ? 1 : 0;
94
+ // Wait rather than immediately throwing SQLITE_BUSY when another
95
+ // connection (often another process) holds a lock.
96
+ if (this.busyTimeout > 0) {
97
+ db.exec(`PRAGMA busy_timeout = ${this.busyTimeout}`);
88
98
  }
89
- catch {
90
- return 0;
99
+ // Register regexp UDF for $regex operator support.
100
+ // WARNING: Patterns are compiled via JavaScript RegExp. User-supplied patterns that are
101
+ // not validated for catastrophic backtracking (ReDoS) could block the event loop.
102
+ // Avoid using untrusted/unvalidated patterns with $regex in security-sensitive contexts.
103
+ db.function('regexp', { deterministic: true }, (pattern, value) => {
104
+ if (typeof pattern !== 'string' || value === null || value === undefined)
105
+ return 0;
106
+ try {
107
+ return new RegExp(pattern).test(String(value)) ? 1 : 0;
108
+ }
109
+ catch {
110
+ return 0;
111
+ }
112
+ });
113
+ // Register regexp_flags UDF for $regex with $options support
114
+ db.function('regexp_flags', { deterministic: true }, (pattern, flags, value) => {
115
+ if (typeof pattern !== 'string' || value === null || value === undefined)
116
+ return 0;
117
+ try {
118
+ const f = typeof flags === 'string' ? flags : '';
119
+ return new RegExp(pattern, f).test(String(value)) ? 1 : 0;
120
+ }
121
+ catch {
122
+ return 0;
123
+ }
124
+ });
125
+ if (this.verbose) {
126
+ console.log(`SQLite database opened (node:sqlite): ${this.filePath}`);
91
127
  }
92
- });
93
- if (this.verbose) {
94
- console.log(`SQLite database opened (node:sqlite): ${this.filePath}`);
95
- }
128
+ return db;
129
+ };
130
+ this.db = this.shareKey
131
+ ? sharedConnections.acquire(this.shareKey, openConnection)
132
+ : openConnection();
96
133
  resolve();
97
134
  }
98
135
  catch (err) {
@@ -166,9 +203,17 @@ export class NodeSqliteAdapter {
166
203
  }
167
204
  if (this.db) {
168
205
  try {
169
- this.db.close();
206
+ // When shared, only the last holder actually closes the handle.
207
+ const isLastHolder = this.shareKey
208
+ ? sharedConnections.release(this.shareKey, this.db)
209
+ : true;
210
+ if (isLastHolder) {
211
+ this.db.close();
212
+ }
170
213
  if (this.verbose) {
171
- console.log(`SQLite database closed (node:sqlite): ${this.filePath}`);
214
+ console.log(isLastHolder
215
+ ? `SQLite database closed (node:sqlite): ${this.filePath}`
216
+ : `SQLite database released (still in use): ${this.filePath}`);
172
217
  }
173
218
  }
174
219
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"node-sqlite.js","sourceRoot":"","sources":["../../src/adapters/node-sqlite.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"node-sqlite.js","sourceRoot":"","sources":["../../src/adapters/node-sqlite.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,19 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.NodeSqliteAdapter = void 0;
7
+ exports.sharedConnectionCount = sharedConnectionCount;
4
8
  const node_sqlite_1 = require("node:sqlite");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const connection_registry_js_1 = require("../utils/connection-registry.js");
11
+ /** Shared `node:sqlite` handles for this process, keyed by connection identity. */
12
+ const sharedConnections = new connection_registry_js_1.ConnectionRegistry();
13
+ /** Exposed for tests and diagnostics. */
14
+ function sharedConnectionCount() {
15
+ return sharedConnections.size;
16
+ }
5
17
  /**
6
18
  * Database adapter backed by Node.js's built-in `node:sqlite` module — no native
7
19
  * addon, no prebuilt binary, no `node-gyp`. Requires Node.js 22.5.0+ (the module
@@ -38,18 +50,34 @@ class NodeSqliteAdapter {
38
50
  constructor(dbPathOrOptions) {
39
51
  this.db = null;
40
52
  this.openPromise = null;
53
+ let shared;
41
54
  if (typeof dbPathOrOptions === 'string') {
42
55
  this.filePath = dbPathOrOptions;
43
56
  this.verbose = false;
44
57
  this.readOnly = false;
45
- this.WAL = false;
58
+ this.WAL = true;
59
+ this.busyTimeout = connection_registry_js_1.DEFAULT_BUSY_TIMEOUT_MS;
60
+ shared = false;
46
61
  }
47
62
  else {
48
63
  this.filePath = dbPathOrOptions.filePath;
49
64
  this.verbose = dbPathOrOptions.verbose || false;
50
65
  this.readOnly = dbPathOrOptions.readOnly || false;
51
- this.WAL = dbPathOrOptions.WAL || true;
52
- }
66
+ this.WAL = dbPathOrOptions.WAL ?? true;
67
+ this.busyTimeout = dbPathOrOptions.busyTimeout ?? connection_registry_js_1.DEFAULT_BUSY_TIMEOUT_MS;
68
+ shared = dbPathOrOptions.shared ?? false;
69
+ }
70
+ this.shareKey =
71
+ shared && (0, connection_registry_js_1.isShareablePath)(this.filePath)
72
+ ? (0, connection_registry_js_1.buildConnectionKey)({
73
+ backend: 'node:sqlite',
74
+ filePath: node_path_1.default.resolve(this.filePath),
75
+ readOnly: this.readOnly,
76
+ WAL: this.WAL,
77
+ busyTimeout: this.busyTimeout,
78
+ verbose: this.verbose,
79
+ })
80
+ : null;
53
81
  }
54
82
  /**
55
83
  * Opens the database connection if it's not already open.
@@ -63,39 +91,52 @@ class NodeSqliteAdapter {
63
91
  }
64
92
  this.openPromise = new Promise((resolve, reject) => {
65
93
  try {
66
- this.db = new node_sqlite_1.DatabaseSync(this.filePath, { readOnly: this.readOnly });
67
- if (this.WAL && !this.readOnly) {
68
- this.db.exec('PRAGMA journal_mode = WAL');
69
- }
70
- // Register regexp UDF for $regex operator support.
71
- // WARNING: Patterns are compiled via JavaScript RegExp. User-supplied patterns that are
72
- // not validated for catastrophic backtracking (ReDoS) could block the event loop.
73
- // Avoid using untrusted/unvalidated patterns with $regex in security-sensitive contexts.
74
- this.db.function('regexp', { deterministic: true }, (pattern, value) => {
75
- if (typeof pattern !== 'string' || value === null || value === undefined)
76
- return 0;
77
- try {
78
- return new RegExp(pattern).test(String(value)) ? 1 : 0;
79
- }
80
- catch {
81
- return 0;
94
+ // Only runs on a genuinely new connection — when sharing, an existing
95
+ // handle already has its pragmas and UDFs applied.
96
+ const openConnection = () => {
97
+ const db = new node_sqlite_1.DatabaseSync(this.filePath, { readOnly: this.readOnly });
98
+ if (this.WAL && !this.readOnly) {
99
+ db.exec('PRAGMA journal_mode = WAL');
82
100
  }
83
- });
84
- // Register regexp_flags UDF for $regex with $options support
85
- this.db.function('regexp_flags', { deterministic: true }, (pattern, flags, value) => {
86
- if (typeof pattern !== 'string' || value === null || value === undefined)
87
- return 0;
88
- try {
89
- const f = typeof flags === 'string' ? flags : '';
90
- return new RegExp(pattern, f).test(String(value)) ? 1 : 0;
101
+ // Wait rather than immediately throwing SQLITE_BUSY when another
102
+ // connection (often another process) holds a lock.
103
+ if (this.busyTimeout > 0) {
104
+ db.exec(`PRAGMA busy_timeout = ${this.busyTimeout}`);
91
105
  }
92
- catch {
93
- return 0;
106
+ // Register regexp UDF for $regex operator support.
107
+ // WARNING: Patterns are compiled via JavaScript RegExp. User-supplied patterns that are
108
+ // not validated for catastrophic backtracking (ReDoS) could block the event loop.
109
+ // Avoid using untrusted/unvalidated patterns with $regex in security-sensitive contexts.
110
+ db.function('regexp', { deterministic: true }, (pattern, value) => {
111
+ if (typeof pattern !== 'string' || value === null || value === undefined)
112
+ return 0;
113
+ try {
114
+ return new RegExp(pattern).test(String(value)) ? 1 : 0;
115
+ }
116
+ catch {
117
+ return 0;
118
+ }
119
+ });
120
+ // Register regexp_flags UDF for $regex with $options support
121
+ db.function('regexp_flags', { deterministic: true }, (pattern, flags, value) => {
122
+ if (typeof pattern !== 'string' || value === null || value === undefined)
123
+ return 0;
124
+ try {
125
+ const f = typeof flags === 'string' ? flags : '';
126
+ return new RegExp(pattern, f).test(String(value)) ? 1 : 0;
127
+ }
128
+ catch {
129
+ return 0;
130
+ }
131
+ });
132
+ if (this.verbose) {
133
+ console.log(`SQLite database opened (node:sqlite): ${this.filePath}`);
94
134
  }
95
- });
96
- if (this.verbose) {
97
- console.log(`SQLite database opened (node:sqlite): ${this.filePath}`);
98
- }
135
+ return db;
136
+ };
137
+ this.db = this.shareKey
138
+ ? sharedConnections.acquire(this.shareKey, openConnection)
139
+ : openConnection();
99
140
  resolve();
100
141
  }
101
142
  catch (err) {
@@ -169,9 +210,17 @@ class NodeSqliteAdapter {
169
210
  }
170
211
  if (this.db) {
171
212
  try {
172
- this.db.close();
213
+ // When shared, only the last holder actually closes the handle.
214
+ const isLastHolder = this.shareKey
215
+ ? sharedConnections.release(this.shareKey, this.db)
216
+ : true;
217
+ if (isLastHolder) {
218
+ this.db.close();
219
+ }
173
220
  if (this.verbose) {
174
- console.log(`SQLite database closed (node:sqlite): ${this.filePath}`);
221
+ console.log(isLastHolder
222
+ ? `SQLite database closed (node:sqlite): ${this.filePath}`
223
+ : `SQLite database released (still in use): ${this.filePath}`);
175
224
  }
176
225
  }
177
226
  catch (err) {
@@ -352,7 +352,7 @@ class MongoLiteCollection {
352
352
  await this.ensureTable();
353
353
  // Find the document first (only one)
354
354
  const paramsForSelect = [];
355
- const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildWhereClause'](filter, paramsForSelect);
355
+ const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildGuardedWhereClause'](filter, paramsForSelect);
356
356
  const selectSql = `SELECT _id, data FROM "${this.name}" WHERE ${whereClause} LIMIT 1`;
357
357
  const rowToUpdate = await this.db.get(selectSql, paramsForSelect);
358
358
  if (!rowToUpdate && !options.upsert) {
@@ -481,7 +481,10 @@ class MongoLiteCollection {
481
481
  const value = opArgs[path];
482
482
  const currentValue = this.getNestedValue(currentDoc, path);
483
483
  const arr = Array.isArray(currentValue) ? currentValue : [];
484
- const items = typeof value === 'object' && value !== null && '$each' in value && Array.isArray(value.$each)
484
+ const items = typeof value === 'object' &&
485
+ value !== null &&
486
+ '$each' in value &&
487
+ Array.isArray(value.$each)
485
488
  ? value.$each
486
489
  : [value];
487
490
  let changed = false;
@@ -617,7 +620,7 @@ class MongoLiteCollection {
617
620
  async updateMany(filter, update) {
618
621
  await this.ensureTable();
619
622
  const paramsForSelect = [];
620
- const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildWhereClause'](filter, paramsForSelect);
623
+ const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildGuardedWhereClause'](filter, paramsForSelect);
621
624
  const selectSql = `SELECT _id, data FROM "${this.name}" WHERE ${whereClause}`;
622
625
  const rowsToUpdate = await this.db.all(selectSql, paramsForSelect);
623
626
  if (rowsToUpdate.length === 0) {
@@ -715,7 +718,10 @@ class MongoLiteCollection {
715
718
  const value = opArgs[path];
716
719
  const currentValue = this.getNestedValue(currentDoc, path);
717
720
  const arr = Array.isArray(currentValue) ? currentValue : [];
718
- const items = typeof value === 'object' && value !== null && '$each' in value && Array.isArray(value.$each)
721
+ const items = typeof value === 'object' &&
722
+ value !== null &&
723
+ '$each' in value &&
724
+ Array.isArray(value.$each)
719
725
  ? value.$each
720
726
  : [value];
721
727
  let changed = false;
@@ -1039,7 +1045,7 @@ class MongoLiteCollection {
1039
1045
  async deleteOne(filter) {
1040
1046
  await this.ensureTable();
1041
1047
  const paramsForDelete = [];
1042
- const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildWhereClause'](filter, paramsForDelete);
1048
+ const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildGuardedWhereClause'](filter, paramsForDelete);
1043
1049
  // Get the number of documents that would be deleted
1044
1050
  const countSql = `SELECT COUNT(*) as count FROM "${this.name}" WHERE ${whereClause} LIMIT 1`;
1045
1051
  const countResult = await this.db.get(countSql, paramsForDelete);
@@ -1068,7 +1074,7 @@ class MongoLiteCollection {
1068
1074
  async deleteMany(filter) {
1069
1075
  await this.ensureTable();
1070
1076
  const paramsForDelete = [];
1071
- const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildWhereClause'](filter, paramsForDelete);
1077
+ const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildGuardedWhereClause'](filter, paramsForDelete);
1072
1078
  // Get the number of documents that would be deleted
1073
1079
  const countSql = `SELECT COUNT(*) as count FROM "${this.name}" WHERE ${whereClause}`;
1074
1080
  const countResult = await this.db.get(countSql, paramsForDelete);
@@ -1094,11 +1100,40 @@ class MongoLiteCollection {
1094
1100
  async countDocuments(filter = {}) {
1095
1101
  await this.ensureTable();
1096
1102
  const paramsForCount = [];
1097
- const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildWhereClause'](filter, paramsForCount);
1103
+ const whereClause = new findCursor_js_1.FindCursor(this.db, this.name, filter)['buildGuardedWhereClause'](filter, paramsForCount);
1098
1104
  const countSql = `SELECT COUNT(*) as count FROM "${this.name}" WHERE ${whereClause}`;
1099
1105
  const result = await this.db.get(countSql, paramsForCount);
1100
1106
  return result?.count || 0;
1101
1107
  }
1108
+ /**
1109
+ * Finds rows whose stored `data` column is not valid JSON.
1110
+ *
1111
+ * Filtered queries deliberately skip these rows — see
1112
+ * `FindCursor.buildGuardedWhereClause` — so that one corrupted document can't
1113
+ * make the whole collection unqueryable. That keeps reads working, but it also
1114
+ * means such rows quietly stop appearing in results. This is the way to find
1115
+ * them so they can be repaired or deleted (`deleteOne({ _id })` works on them,
1116
+ * since `_id` lookups don't go through json1 functions).
1117
+ *
1118
+ * Intended for integrity checks and ops tooling rather than hot paths: it scans
1119
+ * the table and returns the raw column text, not parsed documents.
1120
+ *
1121
+ * @returns The `_id` and raw stored text of every invalid row.
1122
+ */
1123
+ async findInvalidJson() {
1124
+ await this.ensureTable();
1125
+ return this.db.all(`SELECT _id, data FROM "${this.name}" WHERE json_valid(data) = 0`);
1126
+ }
1127
+ /**
1128
+ * Counts rows whose stored `data` column is not valid JSON.
1129
+ * Cheap enough to run on a schedule as a health check.
1130
+ * @see findInvalidJson
1131
+ */
1132
+ async countInvalidJson() {
1133
+ await this.ensureTable();
1134
+ const result = await this.db.get(`SELECT COUNT(*) as count FROM "${this.name}" WHERE json_valid(data) = 0`);
1135
+ return result?.count || 0;
1136
+ }
1102
1137
  /**
1103
1138
  * Returns the number of documents in the collection.
1104
1139
  * This is an estimate and may not reflect the exact count in concurrent environments.
@@ -1127,9 +1162,7 @@ class MongoLiteCollection {
1127
1162
  return null;
1128
1163
  }
1129
1164
  // Update by _id when available to ensure we modify the exact document we found
1130
- const updateFilter = existingId !== undefined && existingId !== null
1131
- ? { _id: existingId }
1132
- : filter;
1165
+ const updateFilter = existingId !== undefined && existingId !== null ? { _id: existingId } : filter;
1133
1166
  const updateResult = await this.updateOne(updateFilter, update, { upsert });
1134
1167
  if (returnDocument === 'after') {
1135
1168
  const targetId = updateResult.upsertedId ?? existingId;
@@ -1490,7 +1523,8 @@ class MongoLiteCollection {
1490
1523
  const nums = groupDocs
1491
1524
  .map((doc) => this.getNestedValue(doc, fieldName))
1492
1525
  .filter((v) => typeof v === 'number');
1493
- groupResult[field] = nums.length > 0 ? nums.reduce((a, b) => a + b, 0) / nums.length : null;
1526
+ groupResult[field] =
1527
+ nums.length > 0 ? nums.reduce((a, b) => a + b, 0) / nums.length : null;
1494
1528
  }
1495
1529
  break;
1496
1530
  }
@@ -1500,7 +1534,8 @@ class MongoLiteCollection {
1500
1534
  const vals = groupDocs
1501
1535
  .map((doc) => this.getNestedValue(doc, fieldName))
1502
1536
  .filter((v) => v !== undefined && v !== null);
1503
- groupResult[field] = vals.length > 0 ? vals.reduce((a, b) => (a < b ? a : b)) : null;
1537
+ groupResult[field] =
1538
+ vals.length > 0 ? vals.reduce((a, b) => (a < b ? a : b)) : null;
1504
1539
  }
1505
1540
  break;
1506
1541
  }
@@ -1510,7 +1545,8 @@ class MongoLiteCollection {
1510
1545
  const vals = groupDocs
1511
1546
  .map((doc) => this.getNestedValue(doc, fieldName))
1512
1547
  .filter((v) => v !== undefined && v !== null);
1513
- groupResult[field] = vals.length > 0 ? vals.reduce((a, b) => (a > b ? a : b)) : null;
1548
+ groupResult[field] =
1549
+ vals.length > 0 ? vals.reduce((a, b) => (a > b ? a : b)) : null;
1514
1550
  }
1515
1551
  break;
1516
1552
  }
@@ -1606,7 +1642,10 @@ class MongoLiteCollection {
1606
1642
  if (key.startsWith('$'))
1607
1643
  continue;
1608
1644
  const docValue = this.getNestedValue(doc, key);
1609
- if (typeof value === 'object' && value !== null && !(value instanceof RegExp) && !(value instanceof Date)) {
1645
+ if (typeof value === 'object' &&
1646
+ value !== null &&
1647
+ !(value instanceof RegExp) &&
1648
+ !(value instanceof Date)) {
1610
1649
  const ops = value;
1611
1650
  const hasOps = Object.keys(ops).some((k) => k.startsWith('$'));
1612
1651
  if (hasOps) {