@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.
- package/README.md +54 -3
- package/dist/adapters/node-sqlite.d.ts +4 -0
- package/dist/adapters/node-sqlite.js +80 -35
- package/dist/adapters/node-sqlite.js.map +1 -1
- package/dist/bin/mongolite-debug.d.ts +6 -1
- package/dist/bin/mongolite-debug.js +6 -113
- package/dist/bin/mongolite-debug.js.map +1 -1
- package/dist/bin/mongolite.d.ts +13 -0
- package/dist/bin/mongolite.js +172 -0
- package/dist/bin/mongolite.js.map +1 -0
- package/dist/cjs/adapters/node-sqlite.js +84 -35
- package/dist/cjs/bin/mongolite-debug.js +6 -149
- package/dist/cjs/bin/mongolite.js +207 -0
- package/dist/cjs/cli/discover.js +156 -0
- package/dist/cjs/cli/explorer.js +929 -0
- package/dist/cjs/cli/filter-builder.js +276 -0
- package/dist/cjs/cli/format.js +161 -0
- package/dist/cjs/cli/schema.js +211 -0
- package/dist/cjs/cli/ui.js +431 -0
- package/dist/cjs/collection.js +53 -14
- package/dist/cjs/cursors/findCursor.js +108 -19
- package/dist/cjs/db.js +87 -40
- package/dist/cjs/utils/connection-registry.js +122 -0
- package/dist/cli/discover.d.ts +40 -0
- package/dist/cli/discover.js +146 -0
- package/dist/cli/discover.js.map +1 -0
- package/dist/cli/explorer.d.ts +59 -0
- package/dist/cli/explorer.js +923 -0
- package/dist/cli/explorer.js.map +1 -0
- package/dist/cli/filter-builder.d.ts +52 -0
- package/dist/cli/filter-builder.js +268 -0
- package/dist/cli/filter-builder.js.map +1 -0
- package/dist/cli/format.d.ts +29 -0
- package/dist/cli/format.js +153 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/schema.d.ts +58 -0
- package/dist/cli/schema.js +205 -0
- package/dist/cli/schema.js.map +1 -0
- package/dist/cli/ui.d.ts +71 -0
- package/dist/cli/ui.js +379 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/collection.d.ts +25 -0
- package/dist/collection.js +53 -14
- package/dist/collection.js.map +1 -1
- package/dist/cursors/findCursor.d.ts +70 -0
- package/dist/cursors/findCursor.js +108 -19
- package/dist/cursors/findCursor.js.map +1 -1
- package/dist/db.d.ts +23 -0
- package/dist/db.js +85 -39
- package/dist/db.js.map +1 -1
- package/dist/utils/connection-registry.d.ts +74 -0
- package/dist/utils/connection-registry.js +117 -0
- package/dist/utils/connection-registry.js.map +1 -0
- package/package.json +2 -1
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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(
|
|
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) {
|
|
@@ -1,152 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
-
if (k2 === undefined) k2 = k;
|
|
5
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
-
}
|
|
9
|
-
Object.defineProperty(o, k2, desc);
|
|
10
|
-
}) : (function(o, m, k, k2) {
|
|
11
|
-
if (k2 === undefined) k2 = k;
|
|
12
|
-
o[k2] = m[k];
|
|
13
|
-
}));
|
|
14
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
-
}) : function(o, v) {
|
|
17
|
-
o["default"] = v;
|
|
18
|
-
});
|
|
19
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
-
var ownKeys = function(o) {
|
|
21
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
-
var ar = [];
|
|
23
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
-
return ar;
|
|
25
|
-
};
|
|
26
|
-
return ownKeys(o);
|
|
27
|
-
};
|
|
28
|
-
return function (mod) {
|
|
29
|
-
if (mod && mod.__esModule) return mod;
|
|
30
|
-
var result = {};
|
|
31
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
-
__setModuleDefault(result, mod);
|
|
33
|
-
return result;
|
|
34
|
-
};
|
|
35
|
-
})();
|
|
36
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
-
};
|
|
39
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
for (let i = 0; i < args.length; i++) {
|
|
47
|
-
const arg = args[i];
|
|
48
|
-
if (arg === '--verbose' || arg === '-v') {
|
|
49
|
-
options.verbose = true;
|
|
50
|
-
}
|
|
51
|
-
else if (arg === '--collection' || arg === '-c') {
|
|
52
|
-
options.collection = args[++i];
|
|
53
|
-
}
|
|
54
|
-
else if (arg === '--database' || arg === '-d') {
|
|
55
|
-
options.database = args[++i];
|
|
56
|
-
}
|
|
57
|
-
else if (arg === '--help' || arg === '-h') {
|
|
58
|
-
options.help = true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return options;
|
|
62
|
-
}
|
|
63
|
-
function showHelp() {
|
|
64
|
-
console.log('MongoLite Query Debugger');
|
|
65
|
-
console.log('');
|
|
66
|
-
console.log('Interactive debugging tool for MongoLite queries.');
|
|
67
|
-
console.log('');
|
|
68
|
-
console.log('Usage: npx mongolite-debug [options]');
|
|
69
|
-
console.log('');
|
|
70
|
-
console.log('Options:');
|
|
71
|
-
console.log(' -d, --database <path> Database file path (default: ./mongolite.db)');
|
|
72
|
-
console.log(' -c, --collection <name> Initial collection to use');
|
|
73
|
-
console.log(' -v, --verbose Enable verbose output');
|
|
74
|
-
console.log(' -h, --help Show this help');
|
|
75
|
-
console.log('');
|
|
76
|
-
console.log('Interactive Commands:');
|
|
77
|
-
console.log(' .help Show available commands');
|
|
78
|
-
console.log(' .collections List all collections');
|
|
79
|
-
console.log(' .use <collection> Select a collection to work with');
|
|
80
|
-
console.log(' .find <filter> Convert find filter to SQL and execute');
|
|
81
|
-
console.log(' .sql <query> Execute raw SQL query');
|
|
82
|
-
console.log(' .last Show last generated SQL');
|
|
83
|
-
console.log(' .sample [count] Show sample documents from current collection');
|
|
84
|
-
console.log(' .exit Exit the debugger');
|
|
85
|
-
console.log('');
|
|
86
|
-
console.log('Examples:');
|
|
87
|
-
console.log(' npx mongolite-debug -d ./myapp.db -c users');
|
|
88
|
-
console.log(' npx mongolite-debug --verbose');
|
|
89
|
-
console.log('');
|
|
90
|
-
console.log(' # Inside the debugger:');
|
|
91
|
-
console.log(' .use users');
|
|
92
|
-
console.log(' .find {"age": {"$gt": 25}}');
|
|
93
|
-
console.log(' .sql SELECT * FROM users WHERE json_extract(data, "$.age") > 25');
|
|
94
|
-
console.log('');
|
|
95
|
-
}
|
|
96
|
-
async function main() {
|
|
97
|
-
const options = parseArgs();
|
|
98
|
-
if (options.help) {
|
|
99
|
-
showHelp();
|
|
100
|
-
process.exit(0);
|
|
101
|
-
}
|
|
102
|
-
// Set default database path if not provided
|
|
103
|
-
if (!options.database) {
|
|
104
|
-
// Look for common database files in the current directory
|
|
105
|
-
const cwd = process.cwd();
|
|
106
|
-
const commonNames = [
|
|
107
|
-
'mongolite.db',
|
|
108
|
-
'mongolite.sqlite',
|
|
109
|
-
'database.db',
|
|
110
|
-
'database.sqlite',
|
|
111
|
-
'app.db',
|
|
112
|
-
'app.sqlite',
|
|
113
|
-
];
|
|
114
|
-
// Check if any common database files exist
|
|
115
|
-
const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
116
|
-
let foundDb = false;
|
|
117
|
-
for (const name of commonNames) {
|
|
118
|
-
const dbPath = path_1.default.join(cwd, name);
|
|
119
|
-
if (existsSync(dbPath)) {
|
|
120
|
-
options.database = dbPath;
|
|
121
|
-
foundDb = true;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (!foundDb) {
|
|
126
|
-
options.database = path_1.default.join(cwd, 'mongolite.db');
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
console.log('🔍 MongoLite Query Debugger (npx version)');
|
|
130
|
-
console.log(`📁 Database: ${options.database}`);
|
|
131
|
-
if (options.collection) {
|
|
132
|
-
console.log(`📋 Initial collection: ${options.collection}`);
|
|
133
|
-
}
|
|
134
|
-
console.log('');
|
|
135
|
-
try {
|
|
136
|
-
const queryDebugger = new query_debugger_js_1.QueryDebugger(options);
|
|
137
|
-
await queryDebugger.start();
|
|
138
|
-
}
|
|
139
|
-
catch (error) {
|
|
140
|
-
console.error('Fatal error:', error instanceof Error ? error.message : error);
|
|
141
|
-
if (error instanceof Error && error.message.includes('SQLITE_CANTOPEN')) {
|
|
142
|
-
console.error('');
|
|
143
|
-
console.error('💡 Tip: Make sure the database file exists or specify a different path with -d');
|
|
144
|
-
console.error(' Example: npx mongolite-debug -d ./path/to/your/database.db');
|
|
145
|
-
}
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
main().catch((error) => {
|
|
150
|
-
console.error('Unexpected error:', error);
|
|
151
|
-
process.exit(1);
|
|
152
|
-
});
|
|
4
|
+
/**
|
|
5
|
+
* `mongolite-debug` is kept as an alias of `mongolite` so existing scripts and
|
|
6
|
+
* muscle memory keep working. It runs the same guided explorer; pass `--repl`
|
|
7
|
+
* for the original command-driven debugger (`.use`, `.find`, `.sql`, …).
|
|
8
|
+
*/
|
|
9
|
+
require("./mongolite.js");
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.parseArgs = parseArgs;
|
|
38
|
+
/**
|
|
39
|
+
* `mongolite` — the guided explorer for MongoLite databases.
|
|
40
|
+
*
|
|
41
|
+
* Running it with no arguments is the intended path: it finds your database
|
|
42
|
+
* files, lists the collections it finds inside, and walks you through a query.
|
|
43
|
+
* Flags are shortcuts for people who already know where they are going, and
|
|
44
|
+
* `--repl` drops into the older command-driven debugger.
|
|
45
|
+
*/
|
|
46
|
+
const explorer_js_1 = require("../cli/explorer.js");
|
|
47
|
+
const query_debugger_js_1 = require("../debugger/query-debugger.js");
|
|
48
|
+
const ui_js_1 = require("../cli/ui.js");
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const options = { verbose: false, help: false, version: false, repl: false };
|
|
51
|
+
const positional = [];
|
|
52
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
53
|
+
const arg = argv[index];
|
|
54
|
+
switch (arg) {
|
|
55
|
+
case '--database':
|
|
56
|
+
case '-d':
|
|
57
|
+
options.database = argv[++index];
|
|
58
|
+
break;
|
|
59
|
+
case '--collection':
|
|
60
|
+
case '-c':
|
|
61
|
+
options.collection = argv[++index];
|
|
62
|
+
break;
|
|
63
|
+
case '--verbose':
|
|
64
|
+
case '-v':
|
|
65
|
+
options.verbose = true;
|
|
66
|
+
break;
|
|
67
|
+
case '--repl':
|
|
68
|
+
case '--expert':
|
|
69
|
+
options.repl = true;
|
|
70
|
+
break;
|
|
71
|
+
case '--sample':
|
|
72
|
+
options.sampleSize = Number(argv[++index]);
|
|
73
|
+
break;
|
|
74
|
+
case '--page-size':
|
|
75
|
+
options.pageSize = Number(argv[++index]);
|
|
76
|
+
break;
|
|
77
|
+
case '--help':
|
|
78
|
+
case '-h':
|
|
79
|
+
options.help = true;
|
|
80
|
+
break;
|
|
81
|
+
case '--version':
|
|
82
|
+
case '-V':
|
|
83
|
+
options.version = true;
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
if (arg.startsWith('-')) {
|
|
87
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
88
|
+
}
|
|
89
|
+
positional.push(arg);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// `mongolite ./app.db users` is the shape people try first, so support it.
|
|
94
|
+
if (!options.database && positional[0])
|
|
95
|
+
options.database = positional[0];
|
|
96
|
+
if (!options.collection && positional[1])
|
|
97
|
+
options.collection = positional[1];
|
|
98
|
+
return options;
|
|
99
|
+
}
|
|
100
|
+
function showHelp() {
|
|
101
|
+
const lines = [
|
|
102
|
+
`${ui_js_1.style.bold('mongolite')} — explore a MongoLite (SQLite-backed) database`,
|
|
103
|
+
'',
|
|
104
|
+
ui_js_1.style.bold('Usage'),
|
|
105
|
+
' npx mongolite Start the guided explorer',
|
|
106
|
+
' npx mongolite ./app.db Open a specific database',
|
|
107
|
+
' npx mongolite ./app.db users Open a specific collection',
|
|
108
|
+
'',
|
|
109
|
+
ui_js_1.style.bold('Options'),
|
|
110
|
+
' -d, --database <path> Database file to open (otherwise you get a list to pick from)',
|
|
111
|
+
' -c, --collection <name> Collection to start in',
|
|
112
|
+
' --sample <n> Documents to sample when inferring fields (default 200)',
|
|
113
|
+
' --page-size <n> Results shown per page (default 20)',
|
|
114
|
+
' --repl Use the older command-driven debugger instead',
|
|
115
|
+
' -v, --verbose Log the SQL being built',
|
|
116
|
+
' -h, --help Show this help',
|
|
117
|
+
' -V, --version Show the version',
|
|
118
|
+
'',
|
|
119
|
+
ui_js_1.style.bold('In the explorer'),
|
|
120
|
+
' ↑/↓ move · type to filter the list · enter to choose · esc to go back · Ctrl+C to quit',
|
|
121
|
+
'',
|
|
122
|
+
'No MongoDB knowledge is needed: pick a field, pick a condition in plain English,',
|
|
123
|
+
'and the filter it builds is printed so you can learn the syntax as you go.',
|
|
124
|
+
'If you already know it, "Write a MongoDB filter yourself" and "Run raw SQL" are',
|
|
125
|
+
'under Advanced.',
|
|
126
|
+
'',
|
|
127
|
+
];
|
|
128
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Find the package version by walking up from the running script.
|
|
132
|
+
* Deliberately avoids `import.meta` and `__dirname` so the same source compiles
|
|
133
|
+
* for both the ESM and CommonJS builds.
|
|
134
|
+
*/
|
|
135
|
+
async function showVersion() {
|
|
136
|
+
const { readFileSync, existsSync } = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
137
|
+
const { dirname, join, parse } = await Promise.resolve().then(() => __importStar(require('node:path')));
|
|
138
|
+
let dir = dirname(process.argv[1] ?? process.cwd());
|
|
139
|
+
const { root } = parse(dir);
|
|
140
|
+
while (dir && dir !== root) {
|
|
141
|
+
const candidate = join(dir, 'package.json');
|
|
142
|
+
if (existsSync(candidate)) {
|
|
143
|
+
try {
|
|
144
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
145
|
+
if (pkg.version) {
|
|
146
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Unreadable package.json - keep walking up.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
dir = dirname(dir);
|
|
155
|
+
}
|
|
156
|
+
process.stdout.write('unknown\n');
|
|
157
|
+
}
|
|
158
|
+
async function main() {
|
|
159
|
+
let options;
|
|
160
|
+
try {
|
|
161
|
+
options = parseArgs(process.argv.slice(2));
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
165
|
+
process.stderr.write('Run with --help to see the available options.\n');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (options.help) {
|
|
170
|
+
showHelp();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (options.version) {
|
|
174
|
+
await showVersion();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (options.repl) {
|
|
178
|
+
const queryDebugger = new query_debugger_js_1.QueryDebugger({
|
|
179
|
+
database: options.database,
|
|
180
|
+
collection: options.collection,
|
|
181
|
+
verbose: options.verbose,
|
|
182
|
+
});
|
|
183
|
+
await queryDebugger.start();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const explorer = new explorer_js_1.Explorer({
|
|
187
|
+
database: options.database,
|
|
188
|
+
collection: options.collection,
|
|
189
|
+
verbose: options.verbose,
|
|
190
|
+
sampleSize: Number.isFinite(options.sampleSize) ? options.sampleSize : undefined,
|
|
191
|
+
pageSize: Number.isFinite(options.pageSize) ? options.pageSize : undefined,
|
|
192
|
+
});
|
|
193
|
+
await explorer.run();
|
|
194
|
+
}
|
|
195
|
+
main()
|
|
196
|
+
.then(() => {
|
|
197
|
+
(0, ui_js_1.closePrompts)();
|
|
198
|
+
process.exit(0);
|
|
199
|
+
})
|
|
200
|
+
.catch((error) => {
|
|
201
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
202
|
+
process.stderr.write(`\n✖ ${text}\n`);
|
|
203
|
+
if (text.includes('SQLITE_CANTOPEN') || text.includes('unable to open')) {
|
|
204
|
+
process.stderr.write(' The file may not exist, or may not be readable.\n');
|
|
205
|
+
}
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.findDatabaseFiles = findDatabaseFiles;
|
|
7
|
+
exports.formatBytes = formatBytes;
|
|
8
|
+
exports.formatRelativeTime = formatRelativeTime;
|
|
9
|
+
exports.listCollectionInfo = listCollectionInfo;
|
|
10
|
+
exports.resolveDatabasePath = resolveDatabasePath;
|
|
11
|
+
exports.databaseExists = databaseExists;
|
|
12
|
+
/**
|
|
13
|
+
* Finding things for the user so they do not have to type paths or remember
|
|
14
|
+
* collection names: SQLite files on disk, and MongoLite-shaped tables inside
|
|
15
|
+
* a database.
|
|
16
|
+
*/
|
|
17
|
+
const node_fs_1 = require("node:fs");
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const DB_EXTENSIONS = ['.db', '.sqlite', '.sqlite3', '.db3'];
|
|
20
|
+
const SKIP_DIRS = new Set([
|
|
21
|
+
'node_modules',
|
|
22
|
+
'.git',
|
|
23
|
+
'dist',
|
|
24
|
+
'build',
|
|
25
|
+
'coverage',
|
|
26
|
+
'.next',
|
|
27
|
+
'.cache',
|
|
28
|
+
'vendor',
|
|
29
|
+
'.venv',
|
|
30
|
+
'venv',
|
|
31
|
+
'__pycache__',
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Walk `root` looking for SQLite-looking files, skipping the usual noisy
|
|
35
|
+
* directories. Results are newest-first, since the file you just wrote is
|
|
36
|
+
* almost always the one you want.
|
|
37
|
+
*/
|
|
38
|
+
function findDatabaseFiles(root = process.cwd(), options = {}) {
|
|
39
|
+
const maxDepth = options.depth ?? 2;
|
|
40
|
+
const limit = options.limit ?? 50;
|
|
41
|
+
const found = [];
|
|
42
|
+
const walk = (dir, depth) => {
|
|
43
|
+
if (found.length >= limit || depth > maxDepth)
|
|
44
|
+
return;
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return; // Unreadable directory - nothing useful to say about it.
|
|
51
|
+
}
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (found.length >= limit)
|
|
54
|
+
return;
|
|
55
|
+
const full = node_path_1.default.join(dir, entry.name);
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.'))
|
|
58
|
+
continue;
|
|
59
|
+
walk(full, depth + 1);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!entry.isFile())
|
|
63
|
+
continue;
|
|
64
|
+
if (!DB_EXTENSIONS.includes(node_path_1.default.extname(entry.name).toLowerCase()))
|
|
65
|
+
continue;
|
|
66
|
+
// WAL/SHM siblings are not databases you can open directly.
|
|
67
|
+
if (/-(wal|shm)$/.test(entry.name))
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const stats = (0, node_fs_1.statSync)(full);
|
|
71
|
+
found.push({
|
|
72
|
+
path: full,
|
|
73
|
+
relativePath: node_path_1.default.relative(root, full) || entry.name,
|
|
74
|
+
sizeBytes: stats.size,
|
|
75
|
+
modified: stats.mtime,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// File vanished between readdir and stat - ignore.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
walk(root, 0);
|
|
84
|
+
found.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
function formatBytes(bytes) {
|
|
88
|
+
if (bytes < 1024)
|
|
89
|
+
return `${bytes} B`;
|
|
90
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
91
|
+
let value = bytes / 1024;
|
|
92
|
+
let unit = 0;
|
|
93
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
94
|
+
value /= 1024;
|
|
95
|
+
unit += 1;
|
|
96
|
+
}
|
|
97
|
+
return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
98
|
+
}
|
|
99
|
+
function formatRelativeTime(date, now = new Date()) {
|
|
100
|
+
const seconds = Math.max(0, Math.round((now.getTime() - date.getTime()) / 1000));
|
|
101
|
+
if (seconds < 60)
|
|
102
|
+
return 'just now';
|
|
103
|
+
const minutes = Math.round(seconds / 60);
|
|
104
|
+
if (minutes < 60)
|
|
105
|
+
return `${minutes}m ago`;
|
|
106
|
+
const hours = Math.round(minutes / 60);
|
|
107
|
+
if (hours < 24)
|
|
108
|
+
return `${hours}h ago`;
|
|
109
|
+
const days = Math.round(hours / 24);
|
|
110
|
+
if (days < 30)
|
|
111
|
+
return `${days}d ago`;
|
|
112
|
+
return date.toISOString().slice(0, 10);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* List the tables in the database, flagging which ones look like MongoLite
|
|
116
|
+
* collections (an `_id` column plus a `data` column). Foreign tables are still
|
|
117
|
+
* reported so a user pointed at the wrong file gets a useful message rather
|
|
118
|
+
* than an empty list.
|
|
119
|
+
*/
|
|
120
|
+
async function listCollectionInfo(db) {
|
|
121
|
+
const tables = await db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
122
|
+
const infos = [];
|
|
123
|
+
for (const { name } of tables) {
|
|
124
|
+
let isMongoLite = false;
|
|
125
|
+
try {
|
|
126
|
+
const columns = await db.all(`PRAGMA table_info("${name}")`);
|
|
127
|
+
const columnNames = new Set(columns.map((column) => column.name));
|
|
128
|
+
isMongoLite = columnNames.has('_id') && columnNames.has('data');
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
isMongoLite = false;
|
|
132
|
+
}
|
|
133
|
+
let count = 0;
|
|
134
|
+
try {
|
|
135
|
+
const row = await db.get(`SELECT COUNT(*) as count FROM "${name}"`);
|
|
136
|
+
count = row?.count ?? 0;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
count = 0;
|
|
140
|
+
}
|
|
141
|
+
infos.push({ name, count, isMongoLite });
|
|
142
|
+
}
|
|
143
|
+
return infos;
|
|
144
|
+
}
|
|
145
|
+
/** Resolve a user-supplied database path, allowing `:memory:` and `~` shorthand. */
|
|
146
|
+
function resolveDatabasePath(input) {
|
|
147
|
+
if (input === ':memory:')
|
|
148
|
+
return input;
|
|
149
|
+
const expanded = input.startsWith('~')
|
|
150
|
+
? node_path_1.default.join(process.env.HOME ?? '', input.slice(1))
|
|
151
|
+
: input;
|
|
152
|
+
return node_path_1.default.resolve(expanded);
|
|
153
|
+
}
|
|
154
|
+
function databaseExists(dbPath) {
|
|
155
|
+
return dbPath === ':memory:' || (0, node_fs_1.existsSync)(dbPath);
|
|
156
|
+
}
|