UNPKG

wise-json-db

Version:

Blazing fast, crash-proof embedded JSON database for Node.js with batch operations, TTL, indexes, and segmented checkpointing.

255 lines (239 loc) 12.3 kB
#!/usr/bin/env node /** * explorer/cli.js — CLI Data Explorer для WiseJSON */ const fs = require('fs'); const fsAsync = require('fs/promises'); const path = require('path'); const readline = require('readline'); const WiseJSON = require('../wise-json/index.js'); const logger = require('../wise-json/logger'); const { matchFilter, flattenDocToCsv } = require('../wise-json/collection/utils.js'); const DB_PATH = process.env.WISE_JSON_PATH || path.resolve(process.cwd(), 'wise-json-db-data'); function prettyError(msg, code = 1) { if (process.argv.includes('--json-errors')) { process.stderr.write(JSON.stringify({ error: true, message: msg, code })); } else { logger.error(`Error: ${msg}`); } process.exit(code); } async function assertCollectionExists(collectionName) { const colPath = path.join(DB_PATH, collectionName); try { await fsAsync.access(colPath); } catch { prettyError(`Collection "${collectionName}" does not exist.`); } } async function confirmAction(prompt) { if (process.argv.includes('--force') || process.argv.includes('--yes')) { return true; } const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise(resolve => { rl.question(`${prompt} [y/N] `, answer => { rl.close(); resolve(answer.toLowerCase() === 'y'); }); }); } async function run() { const args = process.argv.slice(2); const command = args[0]; if (!command || ['help', '--help', '-h'].includes(command)) { console.log(` wisejson-explorer <command> [options] Management Commands (require --allow-write): collection-drop <collectionName> Permanently deletes a collection. doc-insert <collectionName> <json> Inserts a single document. doc-remove <collectionName> <docId> Removes a single document by its ID. import-collection <coll> <file> Imports documents from a JSON file. [--mode append|replace] - (Default: append) create-index <coll> <field> [--unique] Creates an index on a field. drop-index <coll> <field> Drops an index from a field. Read-Only Commands: list-collections Lists all collections with document counts. show-collection <coll> Displays documents in a collection. [--limit N] [--offset M] - Pagination [--sort <field>] [--order asc|desc] - Sorting [--filter <JSON_string>] - Filter documents (e.g., '{"age":{"$gt":25}}') [--output json|table|csv] - Output format get-document <coll> <docId> Shows a single document by ID. collection-stats <coll> Shows collection statistics and indexes. list-indexes <coll> Lists indexes for the collection. export-collection <coll> <file> Exports a collection to a file. [--output json|csv] - Output format Global Options: --allow-write Required for any command that modifies data. --force, --yes Skip confirmation prompts for dangerous operations. --json-errors Output errors in JSON format. `); return; } const writeCommands = ['collection-drop', 'doc-insert', 'doc-remove', 'import-collection', 'create-index', 'drop-index']; if (writeCommands.includes(command) && !args.includes('--allow-write')) { prettyError(`Write operation "${command}" requires the --allow-write flag.`); } const db = new WiseJSON(DB_PATH, { ttlCleanupIntervalMs: 0, checkpointIntervalMs: 0, }); try { await db.init(); const collectionName = args[1]; switch (command) { case 'list-collections': { const collections = await db.getCollectionNames(); const result = await Promise.all(collections.map(async (name) => { const col = await db.collection(name); await col.initPromise; return { name, count: await col.count() }; })); console.table(result); break; } case 'doc-insert': { if (!collectionName || !args[2]) prettyError('Usage: doc-insert <collection> <json>'); const col = await db.collection(collectionName); await col.initPromise; try { const doc = JSON.parse(args[2]); const inserted = await col.insert(doc); console.log(JSON.stringify(inserted, null, 2)); } catch (e) { prettyError(`Failed to insert document: ${e.message}`); } break; } case 'import-collection': { if (!collectionName || !args[2]) prettyError('Usage: import-collection <collection> <file>'); const col = await db.collection(collectionName); await col.initPromise; const filename = args[2]; const mode = args.includes('--mode') && args[args.indexOf('--mode') + 1] === 'replace' ? 'replace' : 'append'; await col.importJson(filename, { mode }); logger.log(`Import to "${collectionName}" from ${filename} completed (mode: ${mode}).`); break; } case 'create-index': { if (!collectionName || !args[2]) prettyError('Usage: create-index <collection> <field> [--unique]'); const col = await db.collection(collectionName); await col.initPromise; await col.createIndex(args[2], { unique: args.includes('--unique') }); logger.log(`Index created on ${collectionName}.${args[2]}`); break; } case 'show-collection': { if (!collectionName) prettyError('Usage: show-collection <collection>'); await assertCollectionExists(collectionName); const col = await db.collection(collectionName); await col.initPromise; let limit = 10, offset = 0, sortField = null, sortOrder = 'asc', output = 'json', filter = null; for (let i = 2; i < args.length; i++) { if (args[i] === '--limit' && args[i + 1]) limit = parseInt(args[++i], 10); if (args[i] === '--offset' && args[i + 1]) offset = parseInt(args[++i], 10); if (args[i] === '--sort' && args[i + 1]) sortField = args[++i]; if (args[i] === '--order' && args[i + 1]) sortOrder = args[++i]; if (args[i] === '--output' && args[i + 1]) output = args[++i]; if (args[i] === '--filter' && args[i + 1]) { try { filter = JSON.parse(args[++i]); } catch (e) { prettyError(`Invalid JSON in filter: ${e.message}`); } } } let docs = await col.find(filter || (() => true)); if (sortField) { docs.sort((a, b) => { if (a[sortField] === undefined) return 1; if (b[sortField] === undefined) return -1; if (a[sortField] < b[sortField]) return sortOrder === 'asc' ? -1 : 1; if (a[sortField] > b[sortField]) return sortOrder === 'asc' ? 1 : -1; return 0; }); } docs = docs.slice(offset, offset + limit); if (output === 'json') console.log(JSON.stringify(docs, null, 2)); else if (output === 'csv') console.log(flattenDocToCsv(docs)); else console.table(docs); break; } case 'get-document': { if (!collectionName || !args[2]) prettyError('Usage: get-document <collection> <docId>'); await assertCollectionExists(collectionName); const col = await db.collection(collectionName); await col.initPromise; const doc = await col.getById(args[2]); if (!doc) prettyError(`Document "${args[2]}" not found.`); console.log(JSON.stringify(doc, null, 2)); break; } case 'collection-stats': { if (!collectionName) prettyError('Usage: collection-stats <collection>'); await assertCollectionExists(collectionName); const col = await db.collection(collectionName); await col.initPromise; const stats = await col.stats(); const indexes = await col.getIndexes(); console.log("--- Stats ---"); console.table([stats]); console.log("\n--- Indexes ---"); console.table(indexes.length > 0 ? indexes : [{ status: 'No indexes found' }]); break; } case 'list-indexes': { if (!collectionName) prettyError('Usage: list-indexes <collection>'); await assertCollectionExists(collectionName); const col = await db.collection(collectionName); await col.initPromise; console.log(JSON.stringify(await col.getIndexes(), null, 2)); break; } case 'export-collection': { if (!collectionName || !args[2]) prettyError('Usage: export-collection <collection> <file>'); await assertCollectionExists(collectionName); const col = await db.collection(collectionName); await col.initPromise; const filename = args[2]; const output = args.includes('--output') && args[args.indexOf('--output') + 1] === 'csv' ? 'csv' : 'json'; if (output === 'csv') await col.exportCsv(filename); else await col.exportJson(filename); logger.log(`Collection "${collectionName}" exported to ${filename}`); break; } case 'doc-remove': { if (!collectionName || !args[2]) prettyError('Usage: doc-remove <collection> <docId>'); await assertCollectionExists(collectionName); const col = await db.collection(collectionName); await col.initPromise; const success = await col.remove(args[2]); if (!success) prettyError(`Document "${args[2]}" not found.`); logger.log(`Document "${args[2]}" removed.`); break; } case 'drop-index': { if (!collectionName || !args[2]) prettyError('Usage: drop-index <collection> <field>'); await assertCollectionExists(collectionName); const col = await db.collection(collectionName); await col.initPromise; await col.dropIndex(args[2]); logger.log(`Index on field "${args[2]}" dropped.`); break; } case 'collection-drop': { if (!collectionName) prettyError('Usage: collection-drop <collection>'); await assertCollectionExists(collectionName); if (!(await confirmAction(`Are you sure you want to permanently delete collection "${collectionName}"?`))) { logger.log('Operation cancelled.'); } else { const collectionPath = path.join(DB_PATH, collectionName); await fsAsync.rm(collectionPath, { recursive: true, force: true }); logger.log(`Collection "${collectionName}" dropped.`); } break; } default: prettyError(`Unknown command: "${command}"`); } } finally { if (db) { await db.close(); } } } run().catch(err => { prettyError(err.message); });