@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
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guided MongoLite explorer.
|
|
3
|
+
*
|
|
4
|
+
* The flow is deliberately linear — pick a database, pick a collection, pick
|
|
5
|
+
* something to do — and every step offers real choices derived from the data
|
|
6
|
+
* rather than asking the user to type something they have to know already.
|
|
7
|
+
* MongoDB syntax stays available under "Advanced", and every guided query
|
|
8
|
+
* prints the filter it built, so the CLI teaches the syntax instead of hiding
|
|
9
|
+
* it.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { MongoLite } from '../index.js';
|
|
14
|
+
import { findDatabaseFiles, formatBytes, formatRelativeTime, listCollectionInfo, resolveDatabasePath, databaseExists, } from './discover.js';
|
|
15
|
+
import { defaultColumns, describeField, getPath, inferSchema, } from './schema.js';
|
|
16
|
+
import { combineConditions, describeCondition, operatorsFor, parseValue, parseValueList, } from './filter-builder.js';
|
|
17
|
+
import { renderDocument, renderSchema, renderSql, renderTable, formatCell } from './format.js';
|
|
18
|
+
import { PromptCancelledError, confirm, error, heading, info, multilineText, pause, select, style, success, text, warn, } from './ui.js';
|
|
19
|
+
export class Explorer {
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.client = null;
|
|
23
|
+
this.dbPath = null;
|
|
24
|
+
this.collectionName = null;
|
|
25
|
+
this.schema = null;
|
|
26
|
+
this.lastSql = null;
|
|
27
|
+
this.lastQuery = null;
|
|
28
|
+
/**
|
|
29
|
+
* `-d`/`-c` are a starting point, not a preference. Once they have been
|
|
30
|
+
* honoured they are forgotten, otherwise "open a different database" would
|
|
31
|
+
* take you straight back to the one on the command line.
|
|
32
|
+
*/
|
|
33
|
+
this.startingDatabaseUsed = false;
|
|
34
|
+
this.startingCollectionUsed = false;
|
|
35
|
+
this.sampleSize = options.sampleSize ?? 200;
|
|
36
|
+
this.pageSize = options.pageSize ?? 20;
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------- lifecycle
|
|
39
|
+
async run() {
|
|
40
|
+
info(`${style.bold('MongoLite')} ${style.grey('· interactive explorer')}`);
|
|
41
|
+
info(style.grey('Ctrl+C to quit at any point.\n'));
|
|
42
|
+
try {
|
|
43
|
+
for (;;) {
|
|
44
|
+
if (!this.client) {
|
|
45
|
+
const connected = await this.chooseDatabase();
|
|
46
|
+
if (!connected)
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!this.collectionName) {
|
|
50
|
+
const chosen = await this.chooseCollection();
|
|
51
|
+
if (chosen === 'quit')
|
|
52
|
+
return;
|
|
53
|
+
if (chosen === 'switch-database') {
|
|
54
|
+
await this.disconnect();
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const outcome = await this.collectionMenu();
|
|
59
|
+
if (outcome === 'quit')
|
|
60
|
+
return;
|
|
61
|
+
if (outcome === 'switch-collection') {
|
|
62
|
+
this.collectionName = null;
|
|
63
|
+
this.schema = null;
|
|
64
|
+
}
|
|
65
|
+
if (outcome === 'switch-database') {
|
|
66
|
+
await this.disconnect();
|
|
67
|
+
this.collectionName = null;
|
|
68
|
+
this.schema = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await this.disconnect();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async disconnect() {
|
|
77
|
+
if (this.client) {
|
|
78
|
+
try {
|
|
79
|
+
await this.client.close();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Closing a database we may never have opened is not worth reporting.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this.client = null;
|
|
86
|
+
this.dbPath = null;
|
|
87
|
+
}
|
|
88
|
+
// ------------------------------------------------------------ step 1: file
|
|
89
|
+
async chooseDatabase() {
|
|
90
|
+
// An explicit -d wins the first time round, as long as it is usable.
|
|
91
|
+
if (this.options.database && !this.startingDatabaseUsed) {
|
|
92
|
+
this.startingDatabaseUsed = true;
|
|
93
|
+
const resolved = resolveDatabasePath(this.options.database);
|
|
94
|
+
if (databaseExists(resolved)) {
|
|
95
|
+
if (await this.connect(resolved))
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
warn(`No database at ${style.bold(resolved)} — pick one below.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (;;) {
|
|
103
|
+
const candidates = findDatabaseFiles(process.cwd());
|
|
104
|
+
const choices = candidates.map((candidate) => ({
|
|
105
|
+
label: candidate.relativePath,
|
|
106
|
+
value: candidate.path,
|
|
107
|
+
hint: `${formatBytes(candidate.sizeBytes)} · ${formatRelativeTime(candidate.modified)}`,
|
|
108
|
+
}));
|
|
109
|
+
if (choices.length === 0) {
|
|
110
|
+
warn('No .db/.sqlite files found in this directory.');
|
|
111
|
+
}
|
|
112
|
+
choices.push({ label: 'Enter a path manually…', value: '__manual__' });
|
|
113
|
+
choices.push({ label: 'Quit', value: '__quit__' });
|
|
114
|
+
let picked;
|
|
115
|
+
try {
|
|
116
|
+
picked = await select({
|
|
117
|
+
message: 'Which database do you want to open?',
|
|
118
|
+
choices,
|
|
119
|
+
hint: ' ↑/↓ move · type to filter · enter to open',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (err instanceof PromptCancelledError)
|
|
124
|
+
return false;
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
if (picked === '__quit__')
|
|
128
|
+
return false;
|
|
129
|
+
const target = picked === '__manual__'
|
|
130
|
+
? resolveDatabasePath(await text({
|
|
131
|
+
message: 'Path to the database file',
|
|
132
|
+
defaultValue: path.join(process.cwd(), 'mongolite.db'),
|
|
133
|
+
}))
|
|
134
|
+
: picked;
|
|
135
|
+
if (!databaseExists(target)) {
|
|
136
|
+
error(`No file at ${target}`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (await this.connect(target))
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async connect(dbPath) {
|
|
144
|
+
try {
|
|
145
|
+
const client = new MongoLite(dbPath, { verbose: this.options.verbose });
|
|
146
|
+
await client.connect();
|
|
147
|
+
this.client = client;
|
|
148
|
+
this.dbPath = dbPath;
|
|
149
|
+
success(`Opened ${style.bold(path.relative(process.cwd(), dbPath) || dbPath)}`);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
error(`Could not open that database: ${message(err)}`);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ------------------------------------------------------ step 2: collection
|
|
158
|
+
async chooseCollection() {
|
|
159
|
+
const client = this.requireClient();
|
|
160
|
+
let collections;
|
|
161
|
+
try {
|
|
162
|
+
collections = await listCollectionInfo(client.database);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
error(`Could not read the collections: ${message(err)}`);
|
|
166
|
+
return 'switch-database';
|
|
167
|
+
}
|
|
168
|
+
const usable = collections.filter((collection) => collection.isMongoLite);
|
|
169
|
+
const foreign = collections.filter((collection) => !collection.isMongoLite);
|
|
170
|
+
if (usable.length === 0) {
|
|
171
|
+
warn('This database has no MongoLite collections.');
|
|
172
|
+
if (foreign.length > 0) {
|
|
173
|
+
info(style.grey(` It does contain ${foreign.length} other table(s): ${foreign
|
|
174
|
+
.map((table) => table.name)
|
|
175
|
+
.slice(0, 5)
|
|
176
|
+
.join(', ')}`));
|
|
177
|
+
info(style.grey(' You can still inspect them with the raw SQL option.'));
|
|
178
|
+
}
|
|
179
|
+
const next = await select({
|
|
180
|
+
message: 'What now?',
|
|
181
|
+
choices: [
|
|
182
|
+
{ label: 'Open a different database', value: 'switch-database' },
|
|
183
|
+
{ label: 'Run raw SQL against this one', value: 'sql' },
|
|
184
|
+
{ label: 'Quit', value: 'quit' },
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
if (next === 'sql') {
|
|
188
|
+
await this.runRawSql();
|
|
189
|
+
return this.chooseCollection();
|
|
190
|
+
}
|
|
191
|
+
return next;
|
|
192
|
+
}
|
|
193
|
+
// An explicit -c only counts the first time, and only if it is really there.
|
|
194
|
+
if (this.options.collection && !this.startingCollectionUsed) {
|
|
195
|
+
this.startingCollectionUsed = true;
|
|
196
|
+
const match = usable.find((collection) => collection.name === this.options.collection);
|
|
197
|
+
if (match) {
|
|
198
|
+
await this.useCollection(match.name);
|
|
199
|
+
return 'ok';
|
|
200
|
+
}
|
|
201
|
+
warn(`No collection named "${this.options.collection}" in this database.`);
|
|
202
|
+
}
|
|
203
|
+
const choices = usable.map((collection) => ({
|
|
204
|
+
label: collection.name,
|
|
205
|
+
value: collection.name,
|
|
206
|
+
hint: `${collection.count.toLocaleString()} document${collection.count === 1 ? '' : 's'}`,
|
|
207
|
+
}));
|
|
208
|
+
choices.push({ label: 'Open a different database', value: '__switch__' });
|
|
209
|
+
choices.push({ label: 'Quit', value: '__quit__' });
|
|
210
|
+
let picked;
|
|
211
|
+
try {
|
|
212
|
+
picked = await select({ message: 'Which collection?', choices });
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
if (err instanceof PromptCancelledError)
|
|
216
|
+
return 'switch-database';
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
if (picked === '__quit__')
|
|
220
|
+
return 'quit';
|
|
221
|
+
if (picked === '__switch__')
|
|
222
|
+
return 'switch-database';
|
|
223
|
+
await this.useCollection(picked);
|
|
224
|
+
return 'ok';
|
|
225
|
+
}
|
|
226
|
+
async useCollection(name) {
|
|
227
|
+
this.collectionName = name;
|
|
228
|
+
this.lastQuery = null;
|
|
229
|
+
const collection = this.collection();
|
|
230
|
+
try {
|
|
231
|
+
const sample = (await collection.find({}).limit(this.sampleSize).toArray());
|
|
232
|
+
this.schema = inferSchema(sample);
|
|
233
|
+
const total = await collection.countDocuments({});
|
|
234
|
+
const fieldCount = this.schema.fields.length;
|
|
235
|
+
info(style.grey(` ${total.toLocaleString()} documents · ${fieldCount} field${fieldCount === 1 ? '' : 's'} inferred from ${sample.length.toLocaleString()} sampled`));
|
|
236
|
+
if (total === 0) {
|
|
237
|
+
warn('This collection is empty, so there is not much to explore yet.');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
warn(`Could not sample "${name}": ${message(err)}`);
|
|
242
|
+
this.schema = { sampled: 0, fields: [] };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// -------------------------------------------------------- step 3: what now
|
|
246
|
+
async collectionMenu() {
|
|
247
|
+
const choices = [
|
|
248
|
+
{ label: 'Browse documents', value: 'browse', hint: '— page through everything' },
|
|
249
|
+
{ label: 'Find documents', value: 'find', hint: '— build a filter step by step' },
|
|
250
|
+
{ label: 'Count documents', value: 'count', hint: '— how many match a filter' },
|
|
251
|
+
{ label: 'List the values in a field', value: 'distinct', hint: '— distinct values' },
|
|
252
|
+
{ label: 'Count documents by field', value: 'group', hint: '— e.g. orders per status' },
|
|
253
|
+
{ label: 'Show the fields in this collection', value: 'schema' },
|
|
254
|
+
{ label: 'Show indexes', value: 'indexes' },
|
|
255
|
+
{ label: 'Advanced', value: 'json-filter', separator: true },
|
|
256
|
+
{ label: 'Write a MongoDB filter yourself', value: 'json-filter', hint: '— JSON' },
|
|
257
|
+
{ label: 'Run raw SQL', value: 'sql' },
|
|
258
|
+
{ label: 'Show the SQL for the last query', value: 'last-sql' },
|
|
259
|
+
{ label: ' ', value: 'quit', separator: true },
|
|
260
|
+
{ label: 'Switch collection', value: 'switch-collection' },
|
|
261
|
+
{ label: 'Open a different database', value: 'switch-database' },
|
|
262
|
+
{ label: 'Quit', value: 'quit' },
|
|
263
|
+
];
|
|
264
|
+
let action;
|
|
265
|
+
try {
|
|
266
|
+
action = await select({
|
|
267
|
+
message: `${this.collectionName} — what would you like to do?`,
|
|
268
|
+
choices,
|
|
269
|
+
pageSize: 16,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (err instanceof PromptCancelledError)
|
|
274
|
+
return 'switch-collection';
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
switch (action) {
|
|
279
|
+
case 'browse':
|
|
280
|
+
await this.runQuery({ filter: {}, sort: null, description: 'all documents' });
|
|
281
|
+
break;
|
|
282
|
+
case 'find':
|
|
283
|
+
await this.guidedFind();
|
|
284
|
+
break;
|
|
285
|
+
case 'count':
|
|
286
|
+
await this.countFlow();
|
|
287
|
+
break;
|
|
288
|
+
case 'distinct':
|
|
289
|
+
await this.distinctFlow();
|
|
290
|
+
break;
|
|
291
|
+
case 'group':
|
|
292
|
+
await this.groupFlow();
|
|
293
|
+
break;
|
|
294
|
+
case 'schema':
|
|
295
|
+
await this.showSchema();
|
|
296
|
+
break;
|
|
297
|
+
case 'indexes':
|
|
298
|
+
await this.showIndexes();
|
|
299
|
+
break;
|
|
300
|
+
case 'json-filter':
|
|
301
|
+
await this.jsonFilterFlow();
|
|
302
|
+
break;
|
|
303
|
+
case 'sql':
|
|
304
|
+
await this.runRawSql();
|
|
305
|
+
break;
|
|
306
|
+
case 'last-sql':
|
|
307
|
+
this.showLastSql();
|
|
308
|
+
await pause();
|
|
309
|
+
break;
|
|
310
|
+
case 'switch-collection':
|
|
311
|
+
return 'switch-collection';
|
|
312
|
+
case 'switch-database':
|
|
313
|
+
return 'switch-database';
|
|
314
|
+
case 'quit':
|
|
315
|
+
return 'quit';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
if (!(err instanceof PromptCancelledError)) {
|
|
320
|
+
error(message(err));
|
|
321
|
+
await pause();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return 'stay';
|
|
325
|
+
}
|
|
326
|
+
// ------------------------------------------------------------ guided query
|
|
327
|
+
async guidedFind() {
|
|
328
|
+
const conditions = await this.buildConditions();
|
|
329
|
+
if (conditions === null)
|
|
330
|
+
return;
|
|
331
|
+
let filter = {};
|
|
332
|
+
let description = 'all documents';
|
|
333
|
+
if (conditions.length > 0) {
|
|
334
|
+
const mode = conditions.length === 1
|
|
335
|
+
? 'all'
|
|
336
|
+
: await select({
|
|
337
|
+
message: 'Should documents match…',
|
|
338
|
+
choices: [
|
|
339
|
+
{ label: 'all of these conditions', value: 'all', hint: '— AND' },
|
|
340
|
+
{ label: 'any of these conditions', value: 'any', hint: '— OR' },
|
|
341
|
+
],
|
|
342
|
+
});
|
|
343
|
+
filter = combineConditions(conditions, mode);
|
|
344
|
+
description = conditions
|
|
345
|
+
.map((condition) => describeCondition(condition))
|
|
346
|
+
.join(mode === 'all' ? ' and ' : ' or ');
|
|
347
|
+
}
|
|
348
|
+
const sort = await this.chooseSort();
|
|
349
|
+
await this.runQuery({ filter, sort, description });
|
|
350
|
+
}
|
|
351
|
+
/** Collect conditions until the user is done. Returns null when cancelled. */
|
|
352
|
+
async buildConditions(existing = []) {
|
|
353
|
+
const conditions = [...existing];
|
|
354
|
+
for (;;) {
|
|
355
|
+
if (conditions.length > 0) {
|
|
356
|
+
heading('Conditions so far');
|
|
357
|
+
conditions.forEach((condition, index) => {
|
|
358
|
+
info(` ${index + 1}. ${describeCondition(condition)}`);
|
|
359
|
+
});
|
|
360
|
+
info('');
|
|
361
|
+
}
|
|
362
|
+
const choices = [
|
|
363
|
+
{
|
|
364
|
+
label: conditions.length === 0 ? 'Add a condition' : 'Add another condition',
|
|
365
|
+
value: 'add',
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
label: conditions.length === 0 ? 'Run without any filter' : 'Run the query',
|
|
369
|
+
value: 'run',
|
|
370
|
+
},
|
|
371
|
+
];
|
|
372
|
+
if (conditions.length > 0)
|
|
373
|
+
choices.push({ label: 'Remove a condition', value: 'remove' });
|
|
374
|
+
choices.push({ label: 'Cancel', value: 'cancel' });
|
|
375
|
+
let step;
|
|
376
|
+
try {
|
|
377
|
+
step = await select({ message: 'Build your query', choices });
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
if (err instanceof PromptCancelledError)
|
|
381
|
+
return null;
|
|
382
|
+
throw err;
|
|
383
|
+
}
|
|
384
|
+
if (step === 'cancel')
|
|
385
|
+
return null;
|
|
386
|
+
if (step === 'run')
|
|
387
|
+
return conditions;
|
|
388
|
+
if (step === 'remove') {
|
|
389
|
+
const index = await select({
|
|
390
|
+
message: 'Remove which condition?',
|
|
391
|
+
choices: conditions.map((condition, position) => ({
|
|
392
|
+
label: describeCondition(condition),
|
|
393
|
+
value: position,
|
|
394
|
+
})),
|
|
395
|
+
});
|
|
396
|
+
conditions.splice(index, 1);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const condition = await this.buildOneCondition();
|
|
400
|
+
if (condition)
|
|
401
|
+
conditions.push(condition);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async buildOneCondition() {
|
|
405
|
+
const schema = this.schema;
|
|
406
|
+
if (!schema || schema.fields.length === 0) {
|
|
407
|
+
warn('No fields were found in this collection, so there is nothing to filter on.');
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
let field;
|
|
411
|
+
try {
|
|
412
|
+
field = await select({
|
|
413
|
+
message: 'Which field?',
|
|
414
|
+
choices: [
|
|
415
|
+
...schema.fields.map((candidate) => ({
|
|
416
|
+
label: candidate.path,
|
|
417
|
+
value: candidate,
|
|
418
|
+
hint: describeField(candidate),
|
|
419
|
+
})),
|
|
420
|
+
{ label: 'Type a field name myself…', value: '__custom__' },
|
|
421
|
+
],
|
|
422
|
+
pageSize: 14,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
if (err instanceof PromptCancelledError)
|
|
427
|
+
return null;
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
430
|
+
const resolved = field === '__custom__'
|
|
431
|
+
? {
|
|
432
|
+
path: await text({
|
|
433
|
+
message: 'Field name (dot notation for nested fields, e.g. address.city)',
|
|
434
|
+
}),
|
|
435
|
+
types: ['string'],
|
|
436
|
+
primaryType: 'string',
|
|
437
|
+
count: 0,
|
|
438
|
+
presence: 0,
|
|
439
|
+
examples: [],
|
|
440
|
+
lowCardinality: false,
|
|
441
|
+
}
|
|
442
|
+
: field;
|
|
443
|
+
const operators = operatorsFor(resolved);
|
|
444
|
+
let operatorId;
|
|
445
|
+
try {
|
|
446
|
+
operatorId = await select({
|
|
447
|
+
message: `${resolved.path} …`,
|
|
448
|
+
choices: operators.map((operator) => ({
|
|
449
|
+
label: operator.label,
|
|
450
|
+
value: operator.id,
|
|
451
|
+
hint: operator.hint ? `— ${operator.hint}` : undefined,
|
|
452
|
+
})),
|
|
453
|
+
pageSize: 14,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
if (err instanceof PromptCancelledError)
|
|
458
|
+
return null;
|
|
459
|
+
throw err;
|
|
460
|
+
}
|
|
461
|
+
const operator = operators.find((candidate) => candidate.id === operatorId);
|
|
462
|
+
if (!operator)
|
|
463
|
+
return null;
|
|
464
|
+
// Values are parsed as the element type for array fields, so
|
|
465
|
+
// "tags contains work" compares against the strings inside the array.
|
|
466
|
+
const valueType = resolved.primaryType === 'array'
|
|
467
|
+
? (resolved.elementTypes?.find((type) => type !== 'null') ?? 'string')
|
|
468
|
+
: resolved.primaryType;
|
|
469
|
+
const values = [];
|
|
470
|
+
try {
|
|
471
|
+
if (operator.arity === 'one') {
|
|
472
|
+
values.push(await this.askForValue(resolved, valueType, operator.valuePrompt ?? 'Value'));
|
|
473
|
+
}
|
|
474
|
+
else if (operator.arity === 'two') {
|
|
475
|
+
values.push(await this.askForValue(resolved, valueType, operator.valuePrompt ?? 'From'));
|
|
476
|
+
values.push(await this.askForValue(resolved, valueType, operator.secondValuePrompt ?? 'To'));
|
|
477
|
+
}
|
|
478
|
+
else if (operator.arity === 'many') {
|
|
479
|
+
const raw = await text({
|
|
480
|
+
message: operator.valuePrompt ?? 'Values (comma separated)',
|
|
481
|
+
validate: (input) => {
|
|
482
|
+
try {
|
|
483
|
+
parseValueList(input, valueType);
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
return message(err);
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
values.push(...parseValueList(raw, valueType));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch (err) {
|
|
495
|
+
if (err instanceof PromptCancelledError)
|
|
496
|
+
return null;
|
|
497
|
+
throw err;
|
|
498
|
+
}
|
|
499
|
+
return { field: resolved.path, operator: operatorId, values, fieldType: valueType };
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Ask for one value. When the field has few distinct values in the sample we
|
|
503
|
+
* offer them as a menu — no typing, no guessing at spelling or casing.
|
|
504
|
+
*/
|
|
505
|
+
async askForValue(field, valueType, prompt) {
|
|
506
|
+
if (field.lowCardinality && field.distinctValues && field.distinctValues.length > 1) {
|
|
507
|
+
const picked = await select({
|
|
508
|
+
message: prompt,
|
|
509
|
+
choices: [
|
|
510
|
+
...field.distinctValues.map((value) => ({
|
|
511
|
+
label: formatCell(value),
|
|
512
|
+
value,
|
|
513
|
+
})),
|
|
514
|
+
{ label: 'Something else…', value: '__other__' },
|
|
515
|
+
],
|
|
516
|
+
hint: style.grey(` values seen in ${field.count} sampled document(s)`),
|
|
517
|
+
pageSize: 14,
|
|
518
|
+
});
|
|
519
|
+
if (picked !== '__other__')
|
|
520
|
+
return picked;
|
|
521
|
+
}
|
|
522
|
+
const raw = await text({
|
|
523
|
+
message: prompt,
|
|
524
|
+
hint: field.examples.length
|
|
525
|
+
? style.grey(` examples: ${field.examples.slice(0, 3).map(formatCell).join(', ')}`)
|
|
526
|
+
: undefined,
|
|
527
|
+
validate: (input) => {
|
|
528
|
+
try {
|
|
529
|
+
parseValue(input, valueType);
|
|
530
|
+
return undefined;
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
return message(err);
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
return parseValue(raw, valueType);
|
|
538
|
+
}
|
|
539
|
+
async chooseSort() {
|
|
540
|
+
const schema = this.schema;
|
|
541
|
+
if (!schema || schema.fields.length === 0)
|
|
542
|
+
return null;
|
|
543
|
+
const sortable = schema.fields.filter((field) => ['string', 'number', 'date', 'boolean'].includes(field.primaryType));
|
|
544
|
+
if (sortable.length === 0)
|
|
545
|
+
return null;
|
|
546
|
+
const field = await select({
|
|
547
|
+
message: 'Sort the results by…',
|
|
548
|
+
choices: [
|
|
549
|
+
{ label: "Don't sort", value: null },
|
|
550
|
+
...sortable.map((candidate) => ({
|
|
551
|
+
label: candidate.path,
|
|
552
|
+
value: candidate.path,
|
|
553
|
+
hint: describeField(candidate),
|
|
554
|
+
})),
|
|
555
|
+
],
|
|
556
|
+
pageSize: 12,
|
|
557
|
+
});
|
|
558
|
+
if (!field)
|
|
559
|
+
return null;
|
|
560
|
+
const direction = await select({
|
|
561
|
+
message: `Sort ${field}…`,
|
|
562
|
+
choices: [
|
|
563
|
+
{ label: 'ascending', value: 1, hint: '— A→Z, smallest first, oldest first' },
|
|
564
|
+
{ label: 'descending', value: -1, hint: '— Z→A, largest first, newest first' },
|
|
565
|
+
],
|
|
566
|
+
});
|
|
567
|
+
return { [field]: direction };
|
|
568
|
+
}
|
|
569
|
+
// -------------------------------------------------------------- run & view
|
|
570
|
+
async runQuery(query) {
|
|
571
|
+
this.lastQuery = query;
|
|
572
|
+
const collection = this.collection();
|
|
573
|
+
let total;
|
|
574
|
+
try {
|
|
575
|
+
total = await collection.countDocuments(query.filter);
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
error(`That query could not run: ${message(err)}`);
|
|
579
|
+
await pause();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
heading('Query');
|
|
583
|
+
info(` ${style.grey('matching')} ${query.description}`);
|
|
584
|
+
info(` ${style.grey('filter')} ${JSON.stringify(query.filter)}`);
|
|
585
|
+
if (query.sort)
|
|
586
|
+
info(` ${style.grey('sort')} ${JSON.stringify(query.sort)}`);
|
|
587
|
+
info(` ${style.grey('matches')} ${style.bold(total.toLocaleString())} document(s)`);
|
|
588
|
+
if (total === 0) {
|
|
589
|
+
info('');
|
|
590
|
+
info(style.grey(' Nothing matched. Try a looser condition, or check the field spelling.'));
|
|
591
|
+
await pause();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
let columns = this.schema ? defaultColumns(this.schema) : ['_id'];
|
|
595
|
+
let offset = 0;
|
|
596
|
+
for (;;) {
|
|
597
|
+
const cursor = collection.find(query.filter).skip(offset).limit(this.pageSize);
|
|
598
|
+
if (query.sort)
|
|
599
|
+
cursor.sort(query.sort);
|
|
600
|
+
this.lastSql = cursor.toSQL();
|
|
601
|
+
let docs;
|
|
602
|
+
try {
|
|
603
|
+
docs = (await cursor.toArray());
|
|
604
|
+
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
error(`Could not fetch results: ${message(err)}`);
|
|
607
|
+
await pause();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (docs.length === 0 && offset > 0) {
|
|
611
|
+
info(style.grey(' No more documents.'));
|
|
612
|
+
offset = Math.max(0, offset - this.pageSize);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
info('');
|
|
616
|
+
info(renderTable(docs, { columns, startIndex: offset }));
|
|
617
|
+
info(style.grey(` showing ${offset + 1}-${offset + docs.length} of ${total.toLocaleString()}`));
|
|
618
|
+
info('');
|
|
619
|
+
const choices = [];
|
|
620
|
+
if (offset + docs.length < total) {
|
|
621
|
+
choices.push({ label: `Show the next ${this.pageSize}`, value: 'next' });
|
|
622
|
+
}
|
|
623
|
+
if (offset > 0)
|
|
624
|
+
choices.push({ label: 'Show the previous page', value: 'prev' });
|
|
625
|
+
choices.push({ label: 'View one document in full', value: 'view' });
|
|
626
|
+
choices.push({ label: 'Choose which columns to show', value: 'columns' });
|
|
627
|
+
choices.push({ label: 'Save these results to a JSON file', value: 'export' });
|
|
628
|
+
choices.push({ label: 'Show the SQL this ran', value: 'sql' });
|
|
629
|
+
choices.push({ label: 'Change the filter', value: 'refine' });
|
|
630
|
+
choices.push({ label: 'Back to the menu', value: 'done' });
|
|
631
|
+
let action;
|
|
632
|
+
try {
|
|
633
|
+
action = await select({ message: 'Results', choices, pageSize: 12 });
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
if (err instanceof PromptCancelledError)
|
|
637
|
+
return;
|
|
638
|
+
throw err;
|
|
639
|
+
}
|
|
640
|
+
switch (action) {
|
|
641
|
+
case 'next':
|
|
642
|
+
offset += this.pageSize;
|
|
643
|
+
break;
|
|
644
|
+
case 'prev':
|
|
645
|
+
offset = Math.max(0, offset - this.pageSize);
|
|
646
|
+
break;
|
|
647
|
+
case 'view': {
|
|
648
|
+
const doc = await select({
|
|
649
|
+
message: 'Which document?',
|
|
650
|
+
choices: docs.map((candidate, index) => ({
|
|
651
|
+
label: `${offset + index + 1}. ${formatCell(candidate._id)}`,
|
|
652
|
+
value: candidate,
|
|
653
|
+
hint: columns
|
|
654
|
+
.filter((column) => column !== '_id')
|
|
655
|
+
.slice(0, 2)
|
|
656
|
+
.map((column) => formatCell(getPath(candidate, column)))
|
|
657
|
+
.join(' · '),
|
|
658
|
+
})),
|
|
659
|
+
pageSize: 12,
|
|
660
|
+
});
|
|
661
|
+
heading('Document');
|
|
662
|
+
info(renderDocument(doc));
|
|
663
|
+
await pause();
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case 'columns':
|
|
667
|
+
columns = await this.chooseColumns(columns);
|
|
668
|
+
break;
|
|
669
|
+
case 'export':
|
|
670
|
+
await this.exportDocuments(query, total);
|
|
671
|
+
break;
|
|
672
|
+
case 'sql':
|
|
673
|
+
this.showLastSql();
|
|
674
|
+
await pause();
|
|
675
|
+
break;
|
|
676
|
+
case 'refine':
|
|
677
|
+
return this.guidedFind();
|
|
678
|
+
case 'done':
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async chooseColumns(current) {
|
|
684
|
+
const schema = this.schema;
|
|
685
|
+
if (!schema)
|
|
686
|
+
return current;
|
|
687
|
+
info(style.grey(` Current columns: ${current.join(', ')}`));
|
|
688
|
+
// No defaultValue: `text` substitutes it for a blank line, which would make
|
|
689
|
+
// "blank for all fields" mean "keep what you already have".
|
|
690
|
+
const raw = await text({
|
|
691
|
+
message: 'Columns to show (comma separated, blank for all fields)',
|
|
692
|
+
allowEmpty: true,
|
|
693
|
+
hint: style.grey(` available: ${schema.fields.map((field) => field.path).join(', ')}`),
|
|
694
|
+
});
|
|
695
|
+
if (raw.trim() === '')
|
|
696
|
+
return schema.fields.map((field) => field.path);
|
|
697
|
+
const columns = raw
|
|
698
|
+
.split(',')
|
|
699
|
+
.map((column) => column.trim())
|
|
700
|
+
.filter((column) => column !== '');
|
|
701
|
+
return columns.length > 0 ? columns : current;
|
|
702
|
+
}
|
|
703
|
+
async exportDocuments(query, total) {
|
|
704
|
+
const limitRaw = await text({
|
|
705
|
+
message: `How many documents? (up to ${total})`,
|
|
706
|
+
defaultValue: String(Math.min(total, 1000)),
|
|
707
|
+
validate: (input) => Number.isInteger(Number(input)) && Number(input) > 0 ? undefined : 'Enter a whole number.',
|
|
708
|
+
});
|
|
709
|
+
const limit = Number(limitRaw);
|
|
710
|
+
const file = await text({
|
|
711
|
+
message: 'Save to which file?',
|
|
712
|
+
defaultValue: path.join(process.cwd(), `${this.collectionName}-export.json`),
|
|
713
|
+
});
|
|
714
|
+
const cursor = this.collection().find(query.filter).limit(limit);
|
|
715
|
+
if (query.sort)
|
|
716
|
+
cursor.sort(query.sort);
|
|
717
|
+
const docs = await cursor.toArray();
|
|
718
|
+
const target = path.resolve(file);
|
|
719
|
+
if (existsSync(target)) {
|
|
720
|
+
const overwrite = await confirm(`${target} already exists. Overwrite it?`, false);
|
|
721
|
+
if (!overwrite) {
|
|
722
|
+
info(style.grey(' Not saved.'));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
writeFileSync(target, `${JSON.stringify(docs, null, 2)}\n`, 'utf8');
|
|
727
|
+
success(`Wrote ${docs.length.toLocaleString()} document(s) to ${target}`);
|
|
728
|
+
}
|
|
729
|
+
// ------------------------------------------------------------ other actions
|
|
730
|
+
async countFlow() {
|
|
731
|
+
const conditions = await this.buildConditions();
|
|
732
|
+
if (conditions === null)
|
|
733
|
+
return;
|
|
734
|
+
const mode = conditions.length > 1
|
|
735
|
+
? await select({
|
|
736
|
+
message: 'Should documents match…',
|
|
737
|
+
choices: [
|
|
738
|
+
{ label: 'all of these conditions', value: 'all', hint: '— AND' },
|
|
739
|
+
{ label: 'any of these conditions', value: 'any', hint: '— OR' },
|
|
740
|
+
],
|
|
741
|
+
})
|
|
742
|
+
: 'all';
|
|
743
|
+
const filter = combineConditions(conditions, mode);
|
|
744
|
+
const count = await this.collection().countDocuments(filter);
|
|
745
|
+
heading('Count');
|
|
746
|
+
info(` ${style.grey('filter')} ${JSON.stringify(filter)}`);
|
|
747
|
+
info(` ${style.bold(count.toLocaleString())} document(s) match`);
|
|
748
|
+
await pause();
|
|
749
|
+
}
|
|
750
|
+
async distinctFlow() {
|
|
751
|
+
const field = await this.pickField('Show the distinct values of which field?');
|
|
752
|
+
if (!field)
|
|
753
|
+
return;
|
|
754
|
+
const values = await this.collection().distinct(field);
|
|
755
|
+
heading(`Values of ${field}`);
|
|
756
|
+
if (values.length === 0) {
|
|
757
|
+
info(style.grey(' No values found.'));
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
const shown = values.slice(0, 200);
|
|
761
|
+
shown.forEach((value) => info(` ${formatCell(value)}`));
|
|
762
|
+
info(style.grey(` ${values.length.toLocaleString()} distinct value(s)${values.length > shown.length ? `, showing the first ${shown.length}` : ''}`));
|
|
763
|
+
}
|
|
764
|
+
await pause();
|
|
765
|
+
}
|
|
766
|
+
async groupFlow() {
|
|
767
|
+
const field = await this.pickField('Count documents by which field?');
|
|
768
|
+
if (!field)
|
|
769
|
+
return;
|
|
770
|
+
const results = (await this.collection()
|
|
771
|
+
.aggregate([{ $group: { _id: `$${field}`, count: { $sum: 1 } } }, { $sort: { count: -1 } }])
|
|
772
|
+
.toArray());
|
|
773
|
+
heading(`Documents per ${field}`);
|
|
774
|
+
if (results.length === 0) {
|
|
775
|
+
info(style.grey(' Nothing to count.'));
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
// `$group` names the grouping key `_id`; label it with the real field so
|
|
779
|
+
// the table reads the way the question was asked.
|
|
780
|
+
const rows = results.map((row) => ({ [field]: row._id, count: row.count }));
|
|
781
|
+
info(renderTable(rows, {
|
|
782
|
+
columns: [field, 'count'],
|
|
783
|
+
showRowNumbers: false,
|
|
784
|
+
}));
|
|
785
|
+
info(style.grey(` ${results.length.toLocaleString()} group(s)`));
|
|
786
|
+
}
|
|
787
|
+
await pause();
|
|
788
|
+
}
|
|
789
|
+
async pickField(message_) {
|
|
790
|
+
const schema = this.schema;
|
|
791
|
+
if (!schema || schema.fields.length === 0) {
|
|
792
|
+
warn('No fields were found in this collection.');
|
|
793
|
+
await pause();
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
const picked = await select({
|
|
798
|
+
message: message_,
|
|
799
|
+
choices: [
|
|
800
|
+
...schema.fields.map((field) => ({
|
|
801
|
+
label: field.path,
|
|
802
|
+
value: field.path,
|
|
803
|
+
hint: describeField(field),
|
|
804
|
+
})),
|
|
805
|
+
{ label: 'Type a field name myself…', value: '__custom__' },
|
|
806
|
+
],
|
|
807
|
+
pageSize: 14,
|
|
808
|
+
});
|
|
809
|
+
if (picked !== '__custom__')
|
|
810
|
+
return picked;
|
|
811
|
+
return await text({ message: 'Field name' });
|
|
812
|
+
}
|
|
813
|
+
catch (err) {
|
|
814
|
+
if (err instanceof PromptCancelledError)
|
|
815
|
+
return null;
|
|
816
|
+
throw err;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async showSchema() {
|
|
820
|
+
const schema = this.schema;
|
|
821
|
+
heading(`Fields in ${this.collectionName}`);
|
|
822
|
+
if (!schema) {
|
|
823
|
+
info(style.grey(' Nothing sampled yet.'));
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
info(renderSchema(schema));
|
|
827
|
+
info(style.grey(`\n Inferred from ${schema.sampled.toLocaleString()} sampled document(s) — fields that only appear in rarer documents may be missing.`));
|
|
828
|
+
}
|
|
829
|
+
await pause();
|
|
830
|
+
}
|
|
831
|
+
async showIndexes() {
|
|
832
|
+
heading(`Indexes on ${this.collectionName}`);
|
|
833
|
+
try {
|
|
834
|
+
const indexes = await this.collection().listIndexes().toArray();
|
|
835
|
+
if (indexes.length === 0) {
|
|
836
|
+
info(style.grey(' No indexes beyond the _id primary key.'));
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
for (const index of indexes) {
|
|
840
|
+
info(` ${style.bold(index.name)} ${style.grey(JSON.stringify(index.key))}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch (err) {
|
|
845
|
+
error(message(err));
|
|
846
|
+
}
|
|
847
|
+
await pause();
|
|
848
|
+
}
|
|
849
|
+
// ------------------------------------------------------------------ escape
|
|
850
|
+
// hatches for people who do know MongoDB / SQL
|
|
851
|
+
async jsonFilterFlow() {
|
|
852
|
+
info(style.grey(' Enter a MongoDB-style filter as JSON, e.g. {"age": {"$gt": 25}, "isActive": true}'));
|
|
853
|
+
const raw = await text({
|
|
854
|
+
message: 'Filter',
|
|
855
|
+
defaultValue: '{}',
|
|
856
|
+
validate: (input) => {
|
|
857
|
+
try {
|
|
858
|
+
const parsed = JSON.parse(input);
|
|
859
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
860
|
+
return 'A filter must be a JSON object.';
|
|
861
|
+
}
|
|
862
|
+
return undefined;
|
|
863
|
+
}
|
|
864
|
+
catch (err) {
|
|
865
|
+
return `That is not valid JSON: ${message(err)}`;
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
const filter = JSON.parse(raw);
|
|
870
|
+
const sort = await this.chooseSort();
|
|
871
|
+
await this.runQuery({ filter, sort, description: `filter ${raw}` });
|
|
872
|
+
}
|
|
873
|
+
async runRawSql() {
|
|
874
|
+
const client = this.requireClient();
|
|
875
|
+
info(style.grey(` Documents live in a "data" JSON column, e.g. SELECT _id, json_extract(data, '$.name') FROM "${this.collectionName ?? 'collection'}"`));
|
|
876
|
+
const sql = await multilineText('SQL to run');
|
|
877
|
+
if (sql.trim() === '')
|
|
878
|
+
return;
|
|
879
|
+
try {
|
|
880
|
+
const rows = await client.database.all(sql);
|
|
881
|
+
heading('Rows');
|
|
882
|
+
if (rows.length === 0) {
|
|
883
|
+
info(style.grey(' No rows returned.'));
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
887
|
+
info(renderTable(rows, { columns }));
|
|
888
|
+
info(style.grey(` ${rows.length.toLocaleString()} row(s)`));
|
|
889
|
+
}
|
|
890
|
+
this.lastSql = { sql, params: [] };
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
error(`SQL error: ${message(err)}`);
|
|
894
|
+
}
|
|
895
|
+
await pause();
|
|
896
|
+
}
|
|
897
|
+
showLastSql() {
|
|
898
|
+
heading('Last query');
|
|
899
|
+
if (!this.lastSql) {
|
|
900
|
+
info(style.grey(' Nothing has run yet.'));
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (this.lastQuery) {
|
|
904
|
+
info(`${style.bold('Filter')}\n${JSON.stringify(this.lastQuery.filter, null, 2)}`);
|
|
905
|
+
}
|
|
906
|
+
info(renderSql(this.lastSql.sql, this.lastSql.params));
|
|
907
|
+
}
|
|
908
|
+
// ------------------------------------------------------------------ helpers
|
|
909
|
+
requireClient() {
|
|
910
|
+
if (!this.client)
|
|
911
|
+
throw new Error('No database is open.');
|
|
912
|
+
return this.client;
|
|
913
|
+
}
|
|
914
|
+
collection() {
|
|
915
|
+
if (!this.collectionName)
|
|
916
|
+
throw new Error('No collection is selected.');
|
|
917
|
+
return this.requireClient().collection(this.collectionName);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function message(err) {
|
|
921
|
+
return err instanceof Error ? err.message : String(err);
|
|
922
|
+
}
|
|
923
|
+
//# sourceMappingURL=explorer.js.map
|