@semics-tech/mongolite 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.
Files changed (54) hide show
  1. package/README.md +54 -3
  2. package/dist/adapters/node-sqlite.d.ts +4 -0
  3. package/dist/adapters/node-sqlite.js +80 -35
  4. package/dist/adapters/node-sqlite.js.map +1 -1
  5. package/dist/bin/mongolite-debug.d.ts +6 -1
  6. package/dist/bin/mongolite-debug.js +6 -113
  7. package/dist/bin/mongolite-debug.js.map +1 -1
  8. package/dist/bin/mongolite.d.ts +13 -0
  9. package/dist/bin/mongolite.js +172 -0
  10. package/dist/bin/mongolite.js.map +1 -0
  11. package/dist/cjs/adapters/node-sqlite.js +84 -35
  12. package/dist/cjs/bin/mongolite-debug.js +6 -149
  13. package/dist/cjs/bin/mongolite.js +207 -0
  14. package/dist/cjs/cli/discover.js +156 -0
  15. package/dist/cjs/cli/explorer.js +929 -0
  16. package/dist/cjs/cli/filter-builder.js +276 -0
  17. package/dist/cjs/cli/format.js +161 -0
  18. package/dist/cjs/cli/schema.js +211 -0
  19. package/dist/cjs/cli/ui.js +431 -0
  20. package/dist/cjs/collection.js +53 -14
  21. package/dist/cjs/cursors/findCursor.js +108 -19
  22. package/dist/cjs/db.js +87 -40
  23. package/dist/cjs/utils/connection-registry.js +122 -0
  24. package/dist/cli/discover.d.ts +40 -0
  25. package/dist/cli/discover.js +146 -0
  26. package/dist/cli/discover.js.map +1 -0
  27. package/dist/cli/explorer.d.ts +59 -0
  28. package/dist/cli/explorer.js +923 -0
  29. package/dist/cli/explorer.js.map +1 -0
  30. package/dist/cli/filter-builder.d.ts +52 -0
  31. package/dist/cli/filter-builder.js +268 -0
  32. package/dist/cli/filter-builder.js.map +1 -0
  33. package/dist/cli/format.d.ts +29 -0
  34. package/dist/cli/format.js +153 -0
  35. package/dist/cli/format.js.map +1 -0
  36. package/dist/cli/schema.d.ts +58 -0
  37. package/dist/cli/schema.js +205 -0
  38. package/dist/cli/schema.js.map +1 -0
  39. package/dist/cli/ui.d.ts +71 -0
  40. package/dist/cli/ui.js +379 -0
  41. package/dist/cli/ui.js.map +1 -0
  42. package/dist/collection.d.ts +25 -0
  43. package/dist/collection.js +53 -14
  44. package/dist/collection.js.map +1 -1
  45. package/dist/cursors/findCursor.d.ts +70 -0
  46. package/dist/cursors/findCursor.js +108 -19
  47. package/dist/cursors/findCursor.js.map +1 -1
  48. package/dist/db.d.ts +23 -0
  49. package/dist/db.js +85 -39
  50. package/dist/db.js.map +1 -1
  51. package/dist/utils/connection-registry.d.ts +74 -0
  52. package/dist/utils/connection-registry.js +117 -0
  53. package/dist/utils/connection-registry.js.map +1 -0
  54. package/package.json +2 -1
@@ -0,0 +1,431 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PromptCancelledError = exports.style = exports.isInteractive = void 0;
37
+ exports.displayWidth = displayWidth;
38
+ exports.truncate = truncate;
39
+ exports.terminalWidth = terminalWidth;
40
+ exports.select = select;
41
+ exports.closePrompts = closePrompts;
42
+ exports.question = question;
43
+ exports.text = text;
44
+ exports.multilineText = multilineText;
45
+ exports.confirm = confirm;
46
+ exports.pause = pause;
47
+ exports.heading = heading;
48
+ exports.info = info;
49
+ exports.warn = warn;
50
+ exports.error = error;
51
+ exports.success = success;
52
+ /**
53
+ * Small, dependency-free terminal UI helpers used by the guided MongoLite CLI.
54
+ *
55
+ * Everything degrades gracefully:
56
+ * - When stdin/stdout is a TTY the prompts are interactive (arrow keys, filtering).
57
+ * - When it is not (pipes, CI, dumb terminals) the same prompts fall back to a
58
+ * numbered list read line-by-line, so the CLI stays scriptable.
59
+ */
60
+ const readline = __importStar(require("node:readline"));
61
+ const NO_COLOR = process.env.NO_COLOR !== undefined || process.env.TERM === 'dumb';
62
+ const isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.MONGOLITE_NON_INTERACTIVE);
63
+ exports.isInteractive = isInteractive;
64
+ const wrap = (open, close) => (text) => NO_COLOR || !process.stdout.isTTY ? text : `\u001b[${open}m${text}\u001b[${close}m`;
65
+ exports.style = {
66
+ bold: wrap('1', '22'),
67
+ dim: wrap('2', '22'),
68
+ italic: wrap('3', '23'),
69
+ underline: wrap('4', '24'),
70
+ red: wrap('31', '39'),
71
+ green: wrap('32', '39'),
72
+ yellow: wrap('33', '39'),
73
+ blue: wrap('34', '39'),
74
+ magenta: wrap('35', '39'),
75
+ cyan: wrap('36', '39'),
76
+ grey: wrap('90', '39'),
77
+ };
78
+ /** Visible width of a string, ignoring ANSI escape sequences. */
79
+ function displayWidth(text) {
80
+ return text.replace(/\u001b\[[0-9;]*m/g, '').length;
81
+ }
82
+ /** Truncate to `max` visible characters, appending an ellipsis when cut. */
83
+ function truncate(text, max) {
84
+ if (max <= 0)
85
+ return '';
86
+ if (text.length <= max)
87
+ return text;
88
+ if (max <= 1)
89
+ return text.slice(0, max);
90
+ return `${text.slice(0, max - 1)}…`;
91
+ }
92
+ function terminalWidth() {
93
+ return Math.max(40, process.stdout.columns || 100);
94
+ }
95
+ /** Thrown when the user aborts a prompt with Esc or Ctrl+C. */
96
+ class PromptCancelledError extends Error {
97
+ constructor() {
98
+ super('Prompt cancelled');
99
+ this.name = 'PromptCancelledError';
100
+ }
101
+ }
102
+ exports.PromptCancelledError = PromptCancelledError;
103
+ function selectableIndexes(choices) {
104
+ return choices.map((c, i) => (c.separator ? -1 : i)).filter((i) => i >= 0);
105
+ }
106
+ /**
107
+ * Ask the user to pick one of `choices`.
108
+ * Interactive terminals get an arrow-key menu with type-to-filter; everything
109
+ * else gets a numbered list.
110
+ */
111
+ async function select(options) {
112
+ if (options.choices.length === 0) {
113
+ throw new Error(`Nothing to choose from for: ${options.message}`);
114
+ }
115
+ const selectable = selectableIndexes(options.choices);
116
+ if (selectable.length === 1 && options.choices.length === 1) {
117
+ // Nothing to decide - tell the user what was picked for them and move on.
118
+ const only = options.choices[selectable[0]];
119
+ process.stdout.write(`${exports.style.green('?')} ${exports.style.bold(options.message)} ${exports.style.cyan(only.label)} ${exports.style.grey('(only option)')}\n`);
120
+ return only.value;
121
+ }
122
+ return (0, exports.isInteractive)() ? interactiveSelect(options) : fallbackSelect(options);
123
+ }
124
+ async function interactiveSelect(options) {
125
+ const { message, choices } = options;
126
+ const pageSize = Math.min(options.pageSize ?? 12, Math.max(3, (process.stdout.rows || 24) - 6));
127
+ let filter = '';
128
+ let cursor = 0;
129
+ let renderedLines = 0;
130
+ const visible = () => {
131
+ const lowered = filter.toLowerCase();
132
+ return choices
133
+ .map((choice, index) => ({ choice, index }))
134
+ .filter(({ choice }) => {
135
+ if (!filter)
136
+ return true;
137
+ if (choice.separator)
138
+ return false;
139
+ return `${choice.label} ${choice.hint ?? ''}`.toLowerCase().includes(lowered);
140
+ });
141
+ };
142
+ const selectablePositions = (list) => list.map((entry, pos) => (entry.choice.separator ? -1 : pos)).filter((pos) => pos >= 0);
143
+ const initial = options.initialIndex ?? 0;
144
+ {
145
+ const list = visible();
146
+ const positions = selectablePositions(list);
147
+ const wanted = list.findIndex((entry) => entry.index === initial);
148
+ cursor = positions.includes(wanted) ? wanted : (positions[0] ?? 0);
149
+ }
150
+ const clear = () => {
151
+ if (renderedLines > 0) {
152
+ process.stdout.write(`\u001b[${renderedLines}A`);
153
+ process.stdout.write('\u001b[0J');
154
+ }
155
+ renderedLines = 0;
156
+ };
157
+ const render = () => {
158
+ clear();
159
+ const width = terminalWidth();
160
+ const list = visible();
161
+ const positions = selectablePositions(list);
162
+ if (positions.length && !positions.includes(cursor)) {
163
+ cursor = positions[0];
164
+ }
165
+ const lines = [];
166
+ const filterSuffix = filter ? ` ${exports.style.yellow(`/${filter}`)}` : '';
167
+ lines.push(`${exports.style.green('?')} ${exports.style.bold(message)}${filterSuffix}`);
168
+ lines.push(exports.style.grey(options.hint ?? ' ↑/↓ move · type to filter · enter to select · esc to go back'));
169
+ // Scroll window so the cursor stays visible.
170
+ let start = 0;
171
+ if (list.length > pageSize) {
172
+ start = Math.min(Math.max(0, cursor - Math.floor(pageSize / 2)), list.length - pageSize);
173
+ }
174
+ const window = list.slice(start, start + pageSize);
175
+ if (window.length === 0) {
176
+ lines.push(exports.style.grey(' no matches'));
177
+ }
178
+ window.forEach((entry, offset) => {
179
+ const pos = start + offset;
180
+ const { choice } = entry;
181
+ if (choice.separator) {
182
+ lines.push(exports.style.grey(` ${choice.label}`));
183
+ return;
184
+ }
185
+ const active = pos === cursor;
186
+ const pointer = active ? exports.style.cyan('❯') : ' ';
187
+ const label = active ? exports.style.cyan(choice.label) : choice.label;
188
+ const hint = choice.hint ? ` ${exports.style.grey(choice.hint)}` : '';
189
+ const line = `${pointer} ${label}${hint}`;
190
+ lines.push(displayWidth(line) > width ? truncate(line, width) : line);
191
+ });
192
+ if (list.length > pageSize) {
193
+ lines.push(exports.style.grey(` … ${start + window.length}/${list.length}`));
194
+ }
195
+ process.stdout.write(`${lines.join('\n')}\n`);
196
+ renderedLines = lines.length;
197
+ };
198
+ return new Promise((resolve, reject) => {
199
+ readline.emitKeypressEvents(process.stdin);
200
+ const wasRaw = process.stdin.isRaw;
201
+ process.stdin.setRawMode(true);
202
+ process.stdin.resume();
203
+ const finish = (fn) => {
204
+ process.stdin.removeListener('keypress', onKeypress);
205
+ process.stdin.setRawMode(Boolean(wasRaw));
206
+ process.stdin.pause();
207
+ fn();
208
+ };
209
+ const onKeypress = (str, key) => {
210
+ const list = visible();
211
+ const positions = selectablePositions(list);
212
+ if (key.ctrl && key.name === 'c') {
213
+ clear();
214
+ finish(() => {
215
+ process.stdout.write(exports.style.grey('Cancelled.\n'));
216
+ process.exit(0);
217
+ });
218
+ return;
219
+ }
220
+ switch (key.name) {
221
+ case 'escape':
222
+ clear();
223
+ finish(() => reject(new PromptCancelledError()));
224
+ return;
225
+ case 'up': {
226
+ const at = positions.indexOf(cursor);
227
+ cursor = positions[(at - 1 + positions.length) % positions.length] ?? cursor;
228
+ render();
229
+ return;
230
+ }
231
+ case 'down': {
232
+ const at = positions.indexOf(cursor);
233
+ cursor = positions[(at + 1) % positions.length] ?? cursor;
234
+ render();
235
+ return;
236
+ }
237
+ case 'pageup':
238
+ cursor = positions[0] ?? cursor;
239
+ render();
240
+ return;
241
+ case 'pagedown':
242
+ cursor = positions[positions.length - 1] ?? cursor;
243
+ render();
244
+ return;
245
+ case 'return':
246
+ case 'enter': {
247
+ const chosen = list[cursor];
248
+ if (!chosen || chosen.choice.separator)
249
+ return;
250
+ clear();
251
+ process.stdout.write(`${exports.style.green('?')} ${exports.style.bold(message)} ${exports.style.cyan(chosen.choice.label)}\n`);
252
+ finish(() => resolve(chosen.choice.value));
253
+ return;
254
+ }
255
+ case 'backspace':
256
+ filter = filter.slice(0, -1);
257
+ render();
258
+ return;
259
+ default:
260
+ break;
261
+ }
262
+ if (str && !key.ctrl && !key.meta && str >= ' ' && str <= '~') {
263
+ filter += str;
264
+ render();
265
+ }
266
+ };
267
+ process.stdin.on('keypress', onKeypress);
268
+ render();
269
+ });
270
+ }
271
+ async function fallbackSelect(options) {
272
+ const { message, choices } = options;
273
+ const numbered = choices
274
+ .map((choice, index) => ({ choice, index }))
275
+ .filter((e) => !e.choice.separator);
276
+ process.stdout.write(`\n${exports.style.bold(message)}\n`);
277
+ numbered.forEach((entry, position) => {
278
+ const hint = entry.choice.hint ? ` ${exports.style.grey(entry.choice.hint)}` : '';
279
+ process.stdout.write(` ${position + 1}) ${entry.choice.label}${hint}\n`);
280
+ });
281
+ for (;;) {
282
+ const answer = (await question(`Choose 1-${numbered.length}: `)).trim();
283
+ if (answer === '')
284
+ continue;
285
+ const position = Number.parseInt(answer, 10);
286
+ if (Number.isInteger(position) && position >= 1 && position <= numbered.length) {
287
+ return numbered[position - 1].choice.value;
288
+ }
289
+ // Also accept an exact (case-insensitive) label so scripted input stays readable.
290
+ const byLabel = numbered.find((entry) => entry.choice.label.toLowerCase() === answer.toLowerCase());
291
+ if (byLabel)
292
+ return byLabel.choice.value;
293
+ process.stdout.write(exports.style.red(' Not a valid choice, try again.\n'));
294
+ }
295
+ }
296
+ /**
297
+ * A single readline interface is reused for line input when stdin is not a
298
+ * TTY. Creating one per prompt would work for a human typing, but a *piped*
299
+ * script has all its answers buffered at once: closing the interface after the
300
+ * first question discards every remaining line. Interactive sessions still get
301
+ * a throwaway interface per prompt so they can coexist with the raw-mode
302
+ * keypress handling in `select`.
303
+ */
304
+ let sharedInterface = null;
305
+ let stdinEnded = false;
306
+ /** Lines that arrived before anything asked for them. */
307
+ const bufferedLines = [];
308
+ /** Callbacks waiting for the next line; `null` means stdin ended first. */
309
+ const lineWaiters = [];
310
+ function getSharedInterface() {
311
+ if (!sharedInterface) {
312
+ sharedInterface = readline.createInterface({
313
+ input: process.stdin,
314
+ output: process.stdout,
315
+ });
316
+ // Queue every line as it arrives. Piped input delivers all of its lines
317
+ // immediately, long before the later prompts ask for them.
318
+ sharedInterface.on('line', (line) => {
319
+ const waiter = lineWaiters.shift();
320
+ if (waiter)
321
+ waiter(line);
322
+ else
323
+ bufferedLines.push(line);
324
+ });
325
+ sharedInterface.on('close', () => {
326
+ stdinEnded = true;
327
+ while (lineWaiters.length > 0)
328
+ lineWaiters.shift()?.(null);
329
+ });
330
+ }
331
+ return sharedInterface;
332
+ }
333
+ /** Release the shared interface so the process can exit. */
334
+ function closePrompts() {
335
+ sharedInterface?.close();
336
+ sharedInterface = null;
337
+ }
338
+ /** Read a single line from stdin. */
339
+ function question(prompt) {
340
+ if ((0, exports.isInteractive)()) {
341
+ return new Promise((resolve) => {
342
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
343
+ rl.question(prompt, (answer) => {
344
+ rl.close();
345
+ resolve(answer);
346
+ });
347
+ });
348
+ }
349
+ getSharedInterface();
350
+ process.stdout.write(prompt);
351
+ const buffered = bufferedLines.shift();
352
+ if (buffered !== undefined) {
353
+ process.stdout.write(`${buffered}\n`);
354
+ return Promise.resolve(buffered);
355
+ }
356
+ if (stdinEnded) {
357
+ // Input is exhausted - carrying on would spin forever re-asking.
358
+ process.stdout.write(`\n${exports.style.grey('Input ended.')}\n`);
359
+ process.exit(0);
360
+ }
361
+ return new Promise((resolve) => {
362
+ lineWaiters.push((line) => {
363
+ if (line === null) {
364
+ process.stdout.write(`\n${exports.style.grey('Input ended.')}\n`);
365
+ process.exit(0);
366
+ }
367
+ resolve(line);
368
+ });
369
+ });
370
+ }
371
+ /** Ask for free-text input, re-prompting until `validate` is happy. */
372
+ async function text(options) {
373
+ const suffix = options.defaultValue ? exports.style.grey(` (${options.defaultValue})`) : '';
374
+ if (options.hint)
375
+ process.stdout.write(`${exports.style.grey(options.hint)}\n`);
376
+ for (;;) {
377
+ const raw = await question(`${exports.style.green('?')} ${exports.style.bold(options.message)}${suffix} `);
378
+ const value = raw.trim() === '' ? (options.defaultValue ?? '') : raw.trim();
379
+ if (value === '' && !options.allowEmpty && !options.defaultValue) {
380
+ process.stdout.write(exports.style.red(' A value is required.\n'));
381
+ continue;
382
+ }
383
+ const error = options.validate?.(value);
384
+ if (error) {
385
+ process.stdout.write(`${exports.style.red(` ${error}`)}\n`);
386
+ continue;
387
+ }
388
+ return value;
389
+ }
390
+ }
391
+ /** Ask for multi-line input; a blank line ends the entry. */
392
+ async function multilineText(message) {
393
+ process.stdout.write(`${exports.style.green('?')} ${exports.style.bold(message)}\n`);
394
+ process.stdout.write(exports.style.grey(' Finish with an empty line.\n'));
395
+ const lines = [];
396
+ for (;;) {
397
+ const line = await question(' ');
398
+ if (line.trim() === '')
399
+ break;
400
+ lines.push(line);
401
+ }
402
+ return lines.join('\n');
403
+ }
404
+ async function confirm(message, defaultValue = false) {
405
+ const hint = defaultValue ? 'Y/n' : 'y/N';
406
+ const answer = (await question(`${exports.style.green('?')} ${exports.style.bold(message)} ${exports.style.grey(`(${hint})`)} `))
407
+ .trim()
408
+ .toLowerCase();
409
+ if (answer === '')
410
+ return defaultValue;
411
+ return answer === 'y' || answer === 'yes';
412
+ }
413
+ /** Wait for the user before scrolling more output past them. */
414
+ async function pause(message = 'Press enter to continue') {
415
+ await question(exports.style.grey(`${message} `));
416
+ }
417
+ function heading(text) {
418
+ process.stdout.write(`\n${exports.style.bold(exports.style.underline(text))}\n`);
419
+ }
420
+ function info(text) {
421
+ process.stdout.write(`${text}\n`);
422
+ }
423
+ function warn(text) {
424
+ process.stdout.write(`${exports.style.yellow('!')} ${text}\n`);
425
+ }
426
+ function error(text) {
427
+ process.stdout.write(`${exports.style.red('✖')} ${text}\n`);
428
+ }
429
+ function success(text) {
430
+ process.stdout.write(`${exports.style.green('✔')} ${text}\n`);
431
+ }
@@ -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) {