import * as plugins from '../../tsmdb.plugins.js'; import type { ICommandHandler, IHandlerContext } from '../CommandRouter.js'; import { SessionEngine } from '../../engine/SessionEngine.js'; /** * AdminHandler - Handles administrative commands */ export class AdminHandler implements ICommandHandler { async handle(context: IHandlerContext): Promise { const { command } = context; // Determine which command to handle if (command.ping !== undefined) { return this.handlePing(context); } else if (command.listDatabases !== undefined) { return this.handleListDatabases(context); } else if (command.listCollections !== undefined) { return this.handleListCollections(context); } else if (command.drop !== undefined) { return this.handleDrop(context); } else if (command.dropDatabase !== undefined) { return this.handleDropDatabase(context); } else if (command.create !== undefined) { return this.handleCreate(context); } else if (command.serverStatus !== undefined) { return this.handleServerStatus(context); } else if (command.buildInfo !== undefined) { return this.handleBuildInfo(context); } else if (command.whatsmyuri !== undefined) { return this.handleWhatsMyUri(context); } else if (command.getLog !== undefined) { return this.handleGetLog(context); } else if (command.hostInfo !== undefined) { return this.handleHostInfo(context); } else if (command.replSetGetStatus !== undefined) { return this.handleReplSetGetStatus(context); } else if (command.saslStart !== undefined) { return this.handleSaslStart(context); } else if (command.saslContinue !== undefined) { return this.handleSaslContinue(context); } else if (command.endSessions !== undefined) { return this.handleEndSessions(context); } else if (command.abortTransaction !== undefined) { return this.handleAbortTransaction(context); } else if (command.commitTransaction !== undefined) { return this.handleCommitTransaction(context); } else if (command.collStats !== undefined) { return this.handleCollStats(context); } else if (command.dbStats !== undefined) { return this.handleDbStats(context); } else if (command.connectionStatus !== undefined) { return this.handleConnectionStatus(context); } else if (command.currentOp !== undefined) { return this.handleCurrentOp(context); } else if (command.collMod !== undefined) { return this.handleCollMod(context); } else if (command.renameCollection !== undefined) { return this.handleRenameCollection(context); } return { ok: 0, errmsg: 'Unknown admin command', code: 59, codeName: 'CommandNotFound', }; } /** * Handle ping command */ private async handlePing(context: IHandlerContext): Promise { return { ok: 1 }; } /** * Handle listDatabases command */ private async handleListDatabases(context: IHandlerContext): Promise { const { storage, command } = context; const dbNames = await storage.listDatabases(); const nameOnly = command.nameOnly || false; if (nameOnly) { return { ok: 1, databases: dbNames.map(name => ({ name })), }; } // Build database list with sizes const databases: plugins.bson.Document[] = []; let totalSize = 0; for (const name of dbNames) { const collections = await storage.listCollections(name); let dbSize = 0; for (const collName of collections) { const docs = await storage.findAll(name, collName); // Estimate size (rough approximation) dbSize += docs.reduce((sum, doc) => sum + JSON.stringify(doc).length, 0); } totalSize += dbSize; databases.push({ name, sizeOnDisk: dbSize, empty: dbSize === 0, }); } return { ok: 1, databases, totalSize, totalSizeMb: totalSize / (1024 * 1024), }; } /** * Handle listCollections command */ private async handleListCollections(context: IHandlerContext): Promise { const { storage, database, command } = context; const filter = command.filter || {}; const nameOnly = command.nameOnly || false; const cursor = command.cursor || {}; const batchSize = cursor.batchSize || 101; const collNames = await storage.listCollections(database); let collections: plugins.bson.Document[] = []; for (const name of collNames) { // Apply name filter if (filter.name && filter.name !== name) { // Check regex if (filter.name.$regex) { const regex = new RegExp(filter.name.$regex, filter.name.$options); if (!regex.test(name)) continue; } else { continue; } } if (nameOnly) { collections.push({ name }); } else { collections.push({ name, type: 'collection', options: {}, info: { readOnly: false, uuid: new plugins.bson.UUID(), }, idIndex: { v: 2, key: { _id: 1 }, name: '_id_', }, }); } } return { ok: 1, cursor: { id: plugins.bson.Long.fromNumber(0), ns: `${database}.$cmd.listCollections`, firstBatch: collections, }, }; } /** * Handle drop command (drop collection) */ private async handleDrop(context: IHandlerContext): Promise { const { storage, database, command } = context; const collection = command.drop; const existed = await storage.dropCollection(database, collection); if (!existed) { return { ok: 0, errmsg: `ns not found ${database}.${collection}`, code: 26, codeName: 'NamespaceNotFound', }; } return { ok: 1, ns: `${database}.${collection}` }; } /** * Handle dropDatabase command */ private async handleDropDatabase(context: IHandlerContext): Promise { const { storage, database } = context; await storage.dropDatabase(database); return { ok: 1, dropped: database }; } /** * Handle create command (create collection) */ private async handleCreate(context: IHandlerContext): Promise { const { storage, database, command } = context; const collection = command.create; // Check if already exists const exists = await storage.collectionExists(database, collection); if (exists) { return { ok: 0, errmsg: `Collection ${database}.${collection} already exists.`, code: 48, codeName: 'NamespaceExists', }; } await storage.createCollection(database, collection); return { ok: 1 }; } /** * Handle serverStatus command */ private async handleServerStatus(context: IHandlerContext): Promise { const { server, sessionEngine } = context; const uptime = server.getUptime(); const connections = server.getConnectionCount(); const sessions = sessionEngine.listSessions(); const sessionsWithTxn = sessionEngine.getSessionsWithTransactions(); return { ok: 1, host: `${server.host}:${server.port}`, version: '7.0.0', process: 'tsmdb', pid: process.pid, uptime, uptimeMillis: uptime * 1000, uptimeEstimate: uptime, localTime: new Date(), mem: { resident: Math.floor(process.memoryUsage().rss / (1024 * 1024)), virtual: Math.floor(process.memoryUsage().heapTotal / (1024 * 1024)), supported: true, }, connections: { current: connections, available: 1000 - connections, totalCreated: connections, active: connections, }, logicalSessionRecordCache: { activeSessionsCount: sessions.length, sessionsCollectionJobCount: 0, lastSessionsCollectionJobDurationMillis: 0, lastSessionsCollectionJobTimestamp: new Date(), transactionReaperJobCount: 0, lastTransactionReaperJobDurationMillis: 0, lastTransactionReaperJobTimestamp: new Date(), }, transactions: { retriedCommandsCount: 0, retriedStatementsCount: 0, transactionsCollectionWriteCount: 0, currentActive: sessionsWithTxn.length, currentInactive: 0, currentOpen: sessionsWithTxn.length, totalStarted: sessionsWithTxn.length, totalCommitted: 0, totalAborted: 0, }, network: { bytesIn: 0, bytesOut: 0, numRequests: 0, }, storageEngine: { name: 'tsmdb', supportsCommittedReads: true, persistent: false, }, }; } /** * Handle buildInfo command */ private async handleBuildInfo(context: IHandlerContext): Promise { return { ok: 1, version: '7.0.0', gitVersion: 'tsmdb', modules: [], allocator: 'system', javascriptEngine: 'none', sysInfo: 'deprecated', versionArray: [7, 0, 0, 0], openssl: { running: 'disabled', compiled: 'disabled', }, buildEnvironment: { distmod: 'tsmdb', distarch: process.arch, cc: '', ccflags: '', cxx: '', cxxflags: '', linkflags: '', target_arch: process.arch, target_os: process.platform, }, bits: 64, debug: false, maxBsonObjectSize: 16777216, storageEngines: ['tsmdb'], }; } /** * Handle whatsmyuri command */ private async handleWhatsMyUri(context: IHandlerContext): Promise { const { server } = context; return { ok: 1, you: `127.0.0.1:${server.port}`, }; } /** * Handle getLog command */ private async handleGetLog(context: IHandlerContext): Promise { const { command } = context; if (command.getLog === '*') { return { ok: 1, names: ['global', 'startupWarnings'], }; } return { ok: 1, totalLinesWritten: 0, log: [], }; } /** * Handle hostInfo command */ private async handleHostInfo(context: IHandlerContext): Promise { return { ok: 1, system: { currentTime: new Date(), hostname: 'localhost', cpuAddrSize: 64, memSizeMB: Math.floor(process.memoryUsage().heapTotal / (1024 * 1024)), numCores: 1, cpuArch: process.arch, numaEnabled: false, }, os: { type: process.platform, name: process.platform, version: process.version, }, extra: {}, }; } /** * Handle replSetGetStatus command */ private async handleReplSetGetStatus(context: IHandlerContext): Promise { // We're standalone, not a replica set return { ok: 0, errmsg: 'not running with --replSet', code: 76, codeName: 'NoReplicationEnabled', }; } /** * Handle saslStart command (authentication) */ private async handleSaslStart(context: IHandlerContext): Promise { // We don't require authentication, but we need to respond properly // to let drivers know auth is "successful" return { ok: 1, conversationId: 1, done: true, payload: Buffer.from([]), }; } /** * Handle saslContinue command */ private async handleSaslContinue(context: IHandlerContext): Promise { return { ok: 1, conversationId: 1, done: true, payload: Buffer.from([]), }; } /** * Handle endSessions command */ private async handleEndSessions(context: IHandlerContext): Promise { const { command, sessionEngine } = context; // End each session in the array const sessions = command.endSessions || []; for (const sessionSpec of sessions) { const sessionId = SessionEngine.extractSessionId(sessionSpec); if (sessionId) { await sessionEngine.endSession(sessionId); } } return { ok: 1 }; } /** * Handle abortTransaction command */ private async handleAbortTransaction(context: IHandlerContext): Promise { const { transactionEngine, sessionEngine, txnId, sessionId } = context; if (!txnId) { return { ok: 0, errmsg: 'No transaction started', code: 251, codeName: 'NoSuchTransaction', }; } try { await transactionEngine.abortTransaction(txnId); transactionEngine.endTransaction(txnId); // Update session state if (sessionId) { sessionEngine.endTransaction(sessionId); } return { ok: 1 }; } catch (error: any) { return { ok: 0, errmsg: error.message || 'Abort transaction failed', code: error.code || 1, codeName: error.codeName || 'UnknownError', }; } } /** * Handle commitTransaction command */ private async handleCommitTransaction(context: IHandlerContext): Promise { const { transactionEngine, sessionEngine, txnId, sessionId } = context; if (!txnId) { return { ok: 0, errmsg: 'No transaction started', code: 251, codeName: 'NoSuchTransaction', }; } try { await transactionEngine.commitTransaction(txnId); transactionEngine.endTransaction(txnId); // Update session state if (sessionId) { sessionEngine.endTransaction(sessionId); } return { ok: 1 }; } catch (error: any) { // If commit fails, transaction should be aborted try { await transactionEngine.abortTransaction(txnId); transactionEngine.endTransaction(txnId); if (sessionId) { sessionEngine.endTransaction(sessionId); } } catch { // Ignore abort errors } if (error.code === 112) { // Write conflict return { ok: 0, errmsg: error.message || 'Write conflict during commit', code: 112, codeName: 'WriteConflict', }; } return { ok: 0, errmsg: error.message || 'Commit transaction failed', code: error.code || 1, codeName: error.codeName || 'UnknownError', }; } } /** * Handle collStats command */ private async handleCollStats(context: IHandlerContext): Promise { const { storage, database, command } = context; const collection = command.collStats; const exists = await storage.collectionExists(database, collection); if (!exists) { return { ok: 0, errmsg: `ns not found ${database}.${collection}`, code: 26, codeName: 'NamespaceNotFound', }; } const docs = await storage.findAll(database, collection); const size = docs.reduce((sum, doc) => sum + JSON.stringify(doc).length, 0); const count = docs.length; const avgObjSize = count > 0 ? size / count : 0; const indexes = await storage.getIndexes(database, collection); return { ok: 1, ns: `${database}.${collection}`, count, size, avgObjSize, storageSize: size, totalIndexSize: 0, indexSizes: indexes.reduce((acc: any, idx: any) => { acc[idx.name] = 0; return acc; }, {}), nindexes: indexes.length, }; } /** * Handle dbStats command */ private async handleDbStats(context: IHandlerContext): Promise { const { storage, database } = context; const collections = await storage.listCollections(database); let totalSize = 0; let totalObjects = 0; for (const collName of collections) { const docs = await storage.findAll(database, collName); totalObjects += docs.length; totalSize += docs.reduce((sum, doc) => sum + JSON.stringify(doc).length, 0); } return { ok: 1, db: database, collections: collections.length, views: 0, objects: totalObjects, avgObjSize: totalObjects > 0 ? totalSize / totalObjects : 0, dataSize: totalSize, storageSize: totalSize, indexes: collections.length, // At least _id index per collection indexSize: 0, totalSize, }; } /** * Handle connectionStatus command */ private async handleConnectionStatus(context: IHandlerContext): Promise { return { ok: 1, authInfo: { authenticatedUsers: [], authenticatedUserRoles: [], }, }; } /** * Handle currentOp command */ private async handleCurrentOp(context: IHandlerContext): Promise { return { ok: 1, inprog: [], }; } /** * Handle collMod command */ private async handleCollMod(context: IHandlerContext): Promise { // We don't support modifying collection options, but acknowledge the command return { ok: 1 }; } /** * Handle renameCollection command */ private async handleRenameCollection(context: IHandlerContext): Promise { const { storage, command } = context; const from = command.renameCollection; const to = command.to; const dropTarget = command.dropTarget || false; if (!from || !to) { return { ok: 0, errmsg: 'renameCollection requires both source and target', code: 2, codeName: 'BadValue', }; } // Parse namespace (format: "db.collection") const fromParts = from.split('.'); const toParts = to.split('.'); if (fromParts.length < 2 || toParts.length < 2) { return { ok: 0, errmsg: 'Invalid namespace format', code: 73, codeName: 'InvalidNamespace', }; } const fromDb = fromParts[0]; const fromColl = fromParts.slice(1).join('.'); const toDb = toParts[0]; const toColl = toParts.slice(1).join('.'); // Check if source exists const sourceExists = await storage.collectionExists(fromDb, fromColl); if (!sourceExists) { return { ok: 0, errmsg: `source namespace ${from} does not exist`, code: 26, codeName: 'NamespaceNotFound', }; } // Check if target exists const targetExists = await storage.collectionExists(toDb, toColl); if (targetExists) { if (dropTarget) { await storage.dropCollection(toDb, toColl); } else { return { ok: 0, errmsg: `target namespace ${to} already exists`, code: 48, codeName: 'NamespaceExists', }; } } // Same database rename if (fromDb === toDb) { await storage.renameCollection(fromDb, fromColl, toColl); } else { // Cross-database rename: copy documents then drop source await storage.createCollection(toDb, toColl); const docs = await storage.findAll(fromDb, fromColl); for (const doc of docs) { await storage.insertOne(toDb, toColl, doc); } await storage.dropCollection(fromDb, fromColl); } return { ok: 1 }; } }