@semics-tech/mongolite 1.2.0 → 1.4.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 +91 -20
- 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/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 +8 -1
- package/dist/cjs/cursors/findCursor.js +20 -8
- package/dist/cjs/index.js +108 -1
- package/dist/cjs/mongo-client.js +18 -1
- package/dist/cjs/server.js +164 -0
- package/dist/cjs/sync/commands.js +126 -0
- package/dist/cjs/sync/diff.js +174 -0
- package/dist/cjs/sync/httpSink.js +159 -0
- package/dist/cjs/sync/index.js +40 -0
- package/dist/cjs/sync/mongoExecutor.js +306 -0
- package/dist/cjs/sync/mongoSink.js +161 -0
- package/dist/cjs/sync/outbox.js +358 -0
- package/dist/cjs/sync/protocol.js +177 -0
- package/dist/cjs/sync/replicator.js +747 -0
- package/dist/cjs/sync/shadow.js +178 -0
- package/dist/cjs/sync/types.js +2 -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/cloudflare.d.ts +1 -1
- package/dist/cloudflare.js.map +1 -1
- package/dist/collection.js +8 -1
- package/dist/collection.js.map +1 -1
- package/dist/cursors/findCursor.d.ts +12 -0
- package/dist/cursors/findCursor.js +20 -8
- package/dist/cursors/findCursor.js.map +1 -1
- package/dist/index.d.ts +68 -2
- package/dist/index.js +96 -1
- package/dist/index.js.map +1 -1
- package/dist/mongo-client.d.ts +13 -0
- package/dist/mongo-client.js +18 -1
- package/dist/mongo-client.js.map +1 -1
- package/dist/server.d.ts +97 -0
- package/dist/server.js +158 -0
- package/dist/server.js.map +1 -0
- package/dist/sync/commands.d.ts +49 -0
- package/dist/sync/commands.js +118 -0
- package/dist/sync/commands.js.map +1 -0
- package/dist/sync/diff.d.ts +55 -0
- package/dist/sync/diff.js +169 -0
- package/dist/sync/diff.js.map +1 -0
- package/dist/sync/httpSink.d.ts +93 -0
- package/dist/sync/httpSink.js +155 -0
- package/dist/sync/httpSink.js.map +1 -0
- package/dist/sync/index.d.ts +17 -0
- package/dist/sync/index.js +10 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/mongoExecutor.d.ts +37 -0
- package/dist/sync/mongoExecutor.js +302 -0
- package/dist/sync/mongoExecutor.js.map +1 -0
- package/dist/sync/mongoSink.d.ts +109 -0
- package/dist/sync/mongoSink.js +123 -0
- package/dist/sync/mongoSink.js.map +1 -0
- package/dist/sync/outbox.d.ts +120 -0
- package/dist/sync/outbox.js +355 -0
- package/dist/sync/outbox.js.map +1 -0
- package/dist/sync/protocol.d.ts +93 -0
- package/dist/sync/protocol.js +168 -0
- package/dist/sync/protocol.js.map +1 -0
- package/dist/sync/replicator.d.ts +177 -0
- package/dist/sync/replicator.js +744 -0
- package/dist/sync/replicator.js.map +1 -0
- package/dist/sync/shadow.d.ts +54 -0
- package/dist/sync/shadow.js +173 -0
- package/dist/sync/shadow.js.map +1 -0
- package/dist/sync/types.d.ts +388 -0
- package/dist/sync/types.js +2 -0
- package/dist/sync/types.js.map +1 -0
- package/package.json +16 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SyncShadow = exports.SHADOW_TABLE = void 0;
|
|
4
|
+
exports.projectToLocalShape = projectToLocalShape;
|
|
5
|
+
exports.projectForComparison = projectForComparison;
|
|
6
|
+
/**
|
|
7
|
+
* The shadow store: the last-known **server** state of each replicated document.
|
|
8
|
+
*
|
|
9
|
+
* It exists because the local store cannot represent MongoDB's type system. Documents
|
|
10
|
+
* are persisted as plain `JSON.stringify`, so a `Date` becomes an ISO string, an
|
|
11
|
+
* `ObjectId` becomes hex, and `Binary`/`Decimal128` degrade to unusable structural
|
|
12
|
+
* objects. Pushing a whole local document back would therefore rewrite the upstream
|
|
13
|
+
* document in that degraded form.
|
|
14
|
+
*
|
|
15
|
+
* Keeping a verbatim copy of the server document solves three problems at once:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Conflict base** — the `_v` a compare-and-swap asserts against.
|
|
18
|
+
* 2. **Type preservation** — fields the application never touched locally are simply
|
|
19
|
+
* absent from the diff, so the upstream values survive intact.
|
|
20
|
+
* 3. **Minimal writes** — pushes become `$set`/`$unset` of what actually changed,
|
|
21
|
+
* so a local edit stops clobbering fields other writers changed.
|
|
22
|
+
*
|
|
23
|
+
* Documents are stored as canonical Extended JSON. That encoding is safe **only**
|
|
24
|
+
* because it lives here and never in a collection table: the query, sort and index
|
|
25
|
+
* layer reads collection data with `json_extract` and assumes bare scalars, so
|
|
26
|
+
* encoding values there would break range queries and indexes wholesale.
|
|
27
|
+
*/
|
|
28
|
+
const bson_1 = require("bson");
|
|
29
|
+
exports.SHADOW_TABLE = '__mongolite_sync_shadow__';
|
|
30
|
+
/**
|
|
31
|
+
* Bookkeeping fields replication maintains on upstream documents. They are stripped
|
|
32
|
+
* from the projection so they never reach local documents and never show up as a
|
|
33
|
+
* spurious change to push back.
|
|
34
|
+
*/
|
|
35
|
+
const SYNC_FIELDS = new Set(['_v', '_updatedAt']);
|
|
36
|
+
/** Millisecond epoch as a SQL expression — portable across SQLite builds. */
|
|
37
|
+
const NOW_MS_SQL = `CAST((julianday('now') - 2440587.5) * 86400000.0 AS INTEGER)`;
|
|
38
|
+
/**
|
|
39
|
+
* Flattens a document to exactly the representation the local store would produce.
|
|
40
|
+
*
|
|
41
|
+
* This is deliberately the *same* lossy transform the collection tables apply
|
|
42
|
+
* (`JSON.stringify`), rather than an approximation of it: a server `Date` projects to
|
|
43
|
+
* the identical ISO string the local row holds, so the two compare equal and the field
|
|
44
|
+
* stays out of the diff. Any drift between this and the storage encoding would show up
|
|
45
|
+
* as phantom changes pushed on every sync.
|
|
46
|
+
*/
|
|
47
|
+
function projectToLocalShape(document) {
|
|
48
|
+
return JSON.parse(JSON.stringify(document));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The projection a local document is compared against: the server document flattened
|
|
52
|
+
* to local shape, with replication's own bookkeeping fields removed.
|
|
53
|
+
*/
|
|
54
|
+
function projectForComparison(document) {
|
|
55
|
+
const projected = projectToLocalShape(document);
|
|
56
|
+
for (const field of SYNC_FIELDS)
|
|
57
|
+
delete projected[field];
|
|
58
|
+
return projected;
|
|
59
|
+
}
|
|
60
|
+
class SyncShadow {
|
|
61
|
+
constructor(db, replicator) {
|
|
62
|
+
this.db = db;
|
|
63
|
+
this.replicator = replicator;
|
|
64
|
+
this.schemaReady = null;
|
|
65
|
+
}
|
|
66
|
+
/** Creates the shadow table. Idempotent. */
|
|
67
|
+
async ensureSchema() {
|
|
68
|
+
if (!this.schemaReady) {
|
|
69
|
+
this.schemaReady = this.createSchema().catch((err) => {
|
|
70
|
+
// Let a later call retry rather than caching the failure forever.
|
|
71
|
+
this.schemaReady = null;
|
|
72
|
+
throw err;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return this.schemaReady;
|
|
76
|
+
}
|
|
77
|
+
async createSchema() {
|
|
78
|
+
await this.db.exec(`
|
|
79
|
+
CREATE TABLE IF NOT EXISTS ${exports.SHADOW_TABLE} (
|
|
80
|
+
replicator TEXT NOT NULL,
|
|
81
|
+
collection_name TEXT NOT NULL,
|
|
82
|
+
document_id TEXT NOT NULL,
|
|
83
|
+
base_version INTEGER,
|
|
84
|
+
server_document TEXT,
|
|
85
|
+
updated_at INTEGER NOT NULL,
|
|
86
|
+
PRIMARY KEY (replicator, collection_name, document_id)
|
|
87
|
+
);
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Loads the shadow entries for a set of documents in one collection.
|
|
92
|
+
* Missing ids simply do not appear — those documents have never been upstream.
|
|
93
|
+
*/
|
|
94
|
+
async load(collection, documentIds) {
|
|
95
|
+
await this.ensureSchema();
|
|
96
|
+
const found = new Map();
|
|
97
|
+
if (documentIds.length === 0)
|
|
98
|
+
return found;
|
|
99
|
+
// Chunked to stay well clear of SQLite's bound-parameter limit.
|
|
100
|
+
const chunkSize = 400;
|
|
101
|
+
for (let i = 0; i < documentIds.length; i += chunkSize) {
|
|
102
|
+
const chunk = documentIds.slice(i, i + chunkSize);
|
|
103
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
104
|
+
const rows = await this.db.all(`SELECT document_id, base_version, server_document
|
|
105
|
+
FROM ${exports.SHADOW_TABLE}
|
|
106
|
+
WHERE replicator = ? AND collection_name = ? AND document_id IN (${placeholders})`, [this.replicator, collection, ...chunk]);
|
|
107
|
+
for (const row of rows) {
|
|
108
|
+
const serverDocument = decodeServerDocument(row.server_document);
|
|
109
|
+
if (!serverDocument)
|
|
110
|
+
continue;
|
|
111
|
+
found.set(row.document_id, {
|
|
112
|
+
collection,
|
|
113
|
+
documentId: row.document_id,
|
|
114
|
+
baseVersion: row.base_version === null ? null : Number(row.base_version),
|
|
115
|
+
serverDocument,
|
|
116
|
+
projection: projectForComparison(serverDocument),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return found;
|
|
121
|
+
}
|
|
122
|
+
/** Records the server state of a document after a successful write or a re-read. */
|
|
123
|
+
async put(collection, documentId, baseVersion, serverDocument) {
|
|
124
|
+
await this.ensureSchema();
|
|
125
|
+
await this.db.run(`INSERT INTO ${exports.SHADOW_TABLE}
|
|
126
|
+
(replicator, collection_name, document_id, base_version, server_document, updated_at)
|
|
127
|
+
VALUES (?, ?, ?, ?, ?, ${NOW_MS_SQL})
|
|
128
|
+
ON CONFLICT (replicator, collection_name, document_id) DO UPDATE SET
|
|
129
|
+
base_version = excluded.base_version,
|
|
130
|
+
server_document = excluded.server_document,
|
|
131
|
+
updated_at = excluded.updated_at`, [
|
|
132
|
+
this.replicator,
|
|
133
|
+
collection,
|
|
134
|
+
documentId,
|
|
135
|
+
baseVersion,
|
|
136
|
+
bson_1.EJSON.stringify(serverDocument, { relaxed: false }),
|
|
137
|
+
]);
|
|
138
|
+
}
|
|
139
|
+
/** Drops a document's shadow — it no longer exists upstream. */
|
|
140
|
+
async remove(collection, documentId) {
|
|
141
|
+
await this.ensureSchema();
|
|
142
|
+
await this.db.run(`DELETE FROM ${exports.SHADOW_TABLE}
|
|
143
|
+
WHERE replicator = ? AND collection_name = ? AND document_id = ?`, [this.replicator, collection, documentId]);
|
|
144
|
+
}
|
|
145
|
+
/** Number of shadowed documents, for diagnostics. */
|
|
146
|
+
async size() {
|
|
147
|
+
await this.ensureSchema();
|
|
148
|
+
const row = await this.db.get(`SELECT COUNT(*) AS total FROM ${exports.SHADOW_TABLE} WHERE replicator = ?`, [this.replicator]);
|
|
149
|
+
return Number(row?.total ?? 0);
|
|
150
|
+
}
|
|
151
|
+
/** Forgets every shadow for this replicator, forcing the next push to re-seed. */
|
|
152
|
+
async clear() {
|
|
153
|
+
await this.ensureSchema();
|
|
154
|
+
const result = await this.db.run(`DELETE FROM ${exports.SHADOW_TABLE} WHERE replicator = ?`, [
|
|
155
|
+
this.replicator,
|
|
156
|
+
]);
|
|
157
|
+
return result.changes;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.SyncShadow = SyncShadow;
|
|
161
|
+
/**
|
|
162
|
+
* Decodes a stored server document. Returns `null` if the row is unreadable, so a
|
|
163
|
+
* corrupt shadow degrades to "never seen upstream" — the document is re-seeded rather
|
|
164
|
+
* than crashing replication.
|
|
165
|
+
*/
|
|
166
|
+
function decodeServerDocument(encoded) {
|
|
167
|
+
if (encoded === null || encoded === undefined)
|
|
168
|
+
return null;
|
|
169
|
+
try {
|
|
170
|
+
const parsed = bson_1.EJSON.parse(encoded, { relaxed: false });
|
|
171
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
172
|
+
return null;
|
|
173
|
+
return parsed;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { IDatabaseAdapter } from '../db.js';
|
|
2
|
+
export interface DatabaseCandidate {
|
|
3
|
+
/** Absolute path to the file. */
|
|
4
|
+
path: string;
|
|
5
|
+
/** Path relative to the search root, for display. */
|
|
6
|
+
relativePath: string;
|
|
7
|
+
sizeBytes: number;
|
|
8
|
+
modified: Date;
|
|
9
|
+
}
|
|
10
|
+
export interface FindDatabasesOptions {
|
|
11
|
+
/** How many directory levels below the root to search. Default 2. */
|
|
12
|
+
depth?: number;
|
|
13
|
+
/** Stop after this many files. Default 50. */
|
|
14
|
+
limit?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Walk `root` looking for SQLite-looking files, skipping the usual noisy
|
|
18
|
+
* directories. Results are newest-first, since the file you just wrote is
|
|
19
|
+
* almost always the one you want.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findDatabaseFiles(root?: string, options?: FindDatabasesOptions): DatabaseCandidate[];
|
|
22
|
+
export declare function formatBytes(bytes: number): string;
|
|
23
|
+
export declare function formatRelativeTime(date: Date, now?: Date): string;
|
|
24
|
+
export interface CollectionInfo {
|
|
25
|
+
name: string;
|
|
26
|
+
/** Number of documents (rows) in the collection. */
|
|
27
|
+
count: number;
|
|
28
|
+
/** False for tables that are not shaped like a MongoLite collection. */
|
|
29
|
+
isMongoLite: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* List the tables in the database, flagging which ones look like MongoLite
|
|
33
|
+
* collections (an `_id` column plus a `data` column). Foreign tables are still
|
|
34
|
+
* reported so a user pointed at the wrong file gets a useful message rather
|
|
35
|
+
* than an empty list.
|
|
36
|
+
*/
|
|
37
|
+
export declare function listCollectionInfo(db: IDatabaseAdapter): Promise<CollectionInfo[]>;
|
|
38
|
+
/** Resolve a user-supplied database path, allowing `:memory:` and `~` shorthand. */
|
|
39
|
+
export declare function resolveDatabasePath(input: string): string;
|
|
40
|
+
export declare function databaseExists(dbPath: string): boolean;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding things for the user so they do not have to type paths or remember
|
|
3
|
+
* collection names: SQLite files on disk, and MongoLite-shaped tables inside
|
|
4
|
+
* a database.
|
|
5
|
+
*/
|
|
6
|
+
import { readdirSync, statSync, existsSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
const DB_EXTENSIONS = ['.db', '.sqlite', '.sqlite3', '.db3'];
|
|
9
|
+
const SKIP_DIRS = new Set([
|
|
10
|
+
'node_modules',
|
|
11
|
+
'.git',
|
|
12
|
+
'dist',
|
|
13
|
+
'build',
|
|
14
|
+
'coverage',
|
|
15
|
+
'.next',
|
|
16
|
+
'.cache',
|
|
17
|
+
'vendor',
|
|
18
|
+
'.venv',
|
|
19
|
+
'venv',
|
|
20
|
+
'__pycache__',
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Walk `root` looking for SQLite-looking files, skipping the usual noisy
|
|
24
|
+
* directories. Results are newest-first, since the file you just wrote is
|
|
25
|
+
* almost always the one you want.
|
|
26
|
+
*/
|
|
27
|
+
export function findDatabaseFiles(root = process.cwd(), options = {}) {
|
|
28
|
+
const maxDepth = options.depth ?? 2;
|
|
29
|
+
const limit = options.limit ?? 50;
|
|
30
|
+
const found = [];
|
|
31
|
+
const walk = (dir, depth) => {
|
|
32
|
+
if (found.length >= limit || depth > maxDepth)
|
|
33
|
+
return;
|
|
34
|
+
let entries;
|
|
35
|
+
try {
|
|
36
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return; // Unreadable directory - nothing useful to say about it.
|
|
40
|
+
}
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (found.length >= limit)
|
|
43
|
+
return;
|
|
44
|
+
const full = path.join(dir, entry.name);
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.'))
|
|
47
|
+
continue;
|
|
48
|
+
walk(full, depth + 1);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!entry.isFile())
|
|
52
|
+
continue;
|
|
53
|
+
if (!DB_EXTENSIONS.includes(path.extname(entry.name).toLowerCase()))
|
|
54
|
+
continue;
|
|
55
|
+
// WAL/SHM siblings are not databases you can open directly.
|
|
56
|
+
if (/-(wal|shm)$/.test(entry.name))
|
|
57
|
+
continue;
|
|
58
|
+
try {
|
|
59
|
+
const stats = statSync(full);
|
|
60
|
+
found.push({
|
|
61
|
+
path: full,
|
|
62
|
+
relativePath: path.relative(root, full) || entry.name,
|
|
63
|
+
sizeBytes: stats.size,
|
|
64
|
+
modified: stats.mtime,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// File vanished between readdir and stat - ignore.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
walk(root, 0);
|
|
73
|
+
found.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
export function formatBytes(bytes) {
|
|
77
|
+
if (bytes < 1024)
|
|
78
|
+
return `${bytes} B`;
|
|
79
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
80
|
+
let value = bytes / 1024;
|
|
81
|
+
let unit = 0;
|
|
82
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
83
|
+
value /= 1024;
|
|
84
|
+
unit += 1;
|
|
85
|
+
}
|
|
86
|
+
return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
87
|
+
}
|
|
88
|
+
export function formatRelativeTime(date, now = new Date()) {
|
|
89
|
+
const seconds = Math.max(0, Math.round((now.getTime() - date.getTime()) / 1000));
|
|
90
|
+
if (seconds < 60)
|
|
91
|
+
return 'just now';
|
|
92
|
+
const minutes = Math.round(seconds / 60);
|
|
93
|
+
if (minutes < 60)
|
|
94
|
+
return `${minutes}m ago`;
|
|
95
|
+
const hours = Math.round(minutes / 60);
|
|
96
|
+
if (hours < 24)
|
|
97
|
+
return `${hours}h ago`;
|
|
98
|
+
const days = Math.round(hours / 24);
|
|
99
|
+
if (days < 30)
|
|
100
|
+
return `${days}d ago`;
|
|
101
|
+
return date.toISOString().slice(0, 10);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* List the tables in the database, flagging which ones look like MongoLite
|
|
105
|
+
* collections (an `_id` column plus a `data` column). Foreign tables are still
|
|
106
|
+
* reported so a user pointed at the wrong file gets a useful message rather
|
|
107
|
+
* than an empty list.
|
|
108
|
+
*/
|
|
109
|
+
export async function listCollectionInfo(db) {
|
|
110
|
+
const tables = await db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
111
|
+
const infos = [];
|
|
112
|
+
for (const { name } of tables) {
|
|
113
|
+
let isMongoLite = false;
|
|
114
|
+
try {
|
|
115
|
+
const columns = await db.all(`PRAGMA table_info("${name}")`);
|
|
116
|
+
const columnNames = new Set(columns.map((column) => column.name));
|
|
117
|
+
isMongoLite = columnNames.has('_id') && columnNames.has('data');
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
isMongoLite = false;
|
|
121
|
+
}
|
|
122
|
+
let count = 0;
|
|
123
|
+
try {
|
|
124
|
+
const row = await db.get(`SELECT COUNT(*) as count FROM "${name}"`);
|
|
125
|
+
count = row?.count ?? 0;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
count = 0;
|
|
129
|
+
}
|
|
130
|
+
infos.push({ name, count, isMongoLite });
|
|
131
|
+
}
|
|
132
|
+
return infos;
|
|
133
|
+
}
|
|
134
|
+
/** Resolve a user-supplied database path, allowing `:memory:` and `~` shorthand. */
|
|
135
|
+
export function resolveDatabasePath(input) {
|
|
136
|
+
if (input === ':memory:')
|
|
137
|
+
return input;
|
|
138
|
+
const expanded = input.startsWith('~')
|
|
139
|
+
? path.join(process.env.HOME ?? '', input.slice(1))
|
|
140
|
+
: input;
|
|
141
|
+
return path.resolve(expanded);
|
|
142
|
+
}
|
|
143
|
+
export function databaseExists(dbPath) {
|
|
144
|
+
return dbPath === ':memory:' || existsSync(dbPath);
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=discover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../../src/cli/discover.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface ExplorerOptions {
|
|
2
|
+
database?: string;
|
|
3
|
+
collection?: string;
|
|
4
|
+
verbose?: boolean;
|
|
5
|
+
/** How many documents to sample when inferring the schema. Default 200. */
|
|
6
|
+
sampleSize?: number;
|
|
7
|
+
/** Rows shown per page of results. Default 20. */
|
|
8
|
+
pageSize?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class Explorer {
|
|
11
|
+
private readonly options;
|
|
12
|
+
private client;
|
|
13
|
+
private dbPath;
|
|
14
|
+
private collectionName;
|
|
15
|
+
private schema;
|
|
16
|
+
private lastSql;
|
|
17
|
+
private lastQuery;
|
|
18
|
+
/**
|
|
19
|
+
* `-d`/`-c` are a starting point, not a preference. Once they have been
|
|
20
|
+
* honoured they are forgotten, otherwise "open a different database" would
|
|
21
|
+
* take you straight back to the one on the command line.
|
|
22
|
+
*/
|
|
23
|
+
private startingDatabaseUsed;
|
|
24
|
+
private startingCollectionUsed;
|
|
25
|
+
private readonly sampleSize;
|
|
26
|
+
private readonly pageSize;
|
|
27
|
+
constructor(options?: ExplorerOptions);
|
|
28
|
+
run(): Promise<void>;
|
|
29
|
+
private disconnect;
|
|
30
|
+
private chooseDatabase;
|
|
31
|
+
private connect;
|
|
32
|
+
private chooseCollection;
|
|
33
|
+
private useCollection;
|
|
34
|
+
private collectionMenu;
|
|
35
|
+
private guidedFind;
|
|
36
|
+
/** Collect conditions until the user is done. Returns null when cancelled. */
|
|
37
|
+
private buildConditions;
|
|
38
|
+
private buildOneCondition;
|
|
39
|
+
/**
|
|
40
|
+
* Ask for one value. When the field has few distinct values in the sample we
|
|
41
|
+
* offer them as a menu — no typing, no guessing at spelling or casing.
|
|
42
|
+
*/
|
|
43
|
+
private askForValue;
|
|
44
|
+
private chooseSort;
|
|
45
|
+
private runQuery;
|
|
46
|
+
private chooseColumns;
|
|
47
|
+
private exportDocuments;
|
|
48
|
+
private countFlow;
|
|
49
|
+
private distinctFlow;
|
|
50
|
+
private groupFlow;
|
|
51
|
+
private pickField;
|
|
52
|
+
private showSchema;
|
|
53
|
+
private showIndexes;
|
|
54
|
+
private jsonFilterFlow;
|
|
55
|
+
private runRawSql;
|
|
56
|
+
private showLastSql;
|
|
57
|
+
private requireClient;
|
|
58
|
+
private collection;
|
|
59
|
+
}
|