BREAKING CHANGE(tsmdb): rename CongoDB to TsmDB and relocate/rename wire-protocol server implementation and public exports
This commit is contained in:
614
ts/tsmdb/server/handlers/AdminHandler.ts
Normal file
614
ts/tsmdb/server/handlers/AdminHandler.ts
Normal file
@@ -0,0 +1,614 @@
|
||||
import * as plugins from '../../tsmdb.plugins.js';
|
||||
import type { ICommandHandler, IHandlerContext } from '../CommandRouter.js';
|
||||
|
||||
/**
|
||||
* AdminHandler - Handles administrative commands
|
||||
*/
|
||||
export class AdminHandler implements ICommandHandler {
|
||||
async handle(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
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<plugins.bson.Document> {
|
||||
return { ok: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle listDatabases command
|
||||
*/
|
||||
private async handleListDatabases(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
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<plugins.bson.Document> {
|
||||
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<plugins.bson.Document> {
|
||||
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<plugins.bson.Document> {
|
||||
const { storage, database } = context;
|
||||
|
||||
await storage.dropDatabase(database);
|
||||
|
||||
return { ok: 1, dropped: database };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle create command (create collection)
|
||||
*/
|
||||
private async handleCreate(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
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<plugins.bson.Document> {
|
||||
const { server } = context;
|
||||
|
||||
const uptime = server.getUptime();
|
||||
const connections = server.getConnectionCount();
|
||||
|
||||
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,
|
||||
},
|
||||
network: {
|
||||
bytesIn: 0,
|
||||
bytesOut: 0,
|
||||
numRequests: 0,
|
||||
},
|
||||
storageEngine: {
|
||||
name: 'tsmdb',
|
||||
supportsCommittedReads: true,
|
||||
persistent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle buildInfo command
|
||||
*/
|
||||
private async handleBuildInfo(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
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<plugins.bson.Document> {
|
||||
const { server } = context;
|
||||
return {
|
||||
ok: 1,
|
||||
you: `127.0.0.1:${server.port}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle getLog command
|
||||
*/
|
||||
private async handleGetLog(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
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<plugins.bson.Document> {
|
||||
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<plugins.bson.Document> {
|
||||
// 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<plugins.bson.Document> {
|
||||
// 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<plugins.bson.Document> {
|
||||
return {
|
||||
ok: 1,
|
||||
conversationId: 1,
|
||||
done: true,
|
||||
payload: Buffer.from([]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle endSessions command
|
||||
*/
|
||||
private async handleEndSessions(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
return { ok: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle abortTransaction command
|
||||
*/
|
||||
private async handleAbortTransaction(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
// Transactions are not fully supported, but acknowledge the command
|
||||
return { ok: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle commitTransaction command
|
||||
*/
|
||||
private async handleCommitTransaction(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
// Transactions are not fully supported, but acknowledge the command
|
||||
return { ok: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle collStats command
|
||||
*/
|
||||
private async handleCollStats(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
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<plugins.bson.Document> {
|
||||
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<plugins.bson.Document> {
|
||||
return {
|
||||
ok: 1,
|
||||
authInfo: {
|
||||
authenticatedUsers: [],
|
||||
authenticatedUserRoles: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle currentOp command
|
||||
*/
|
||||
private async handleCurrentOp(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
return {
|
||||
ok: 1,
|
||||
inprog: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle collMod command
|
||||
*/
|
||||
private async handleCollMod(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
// We don't support modifying collection options, but acknowledge the command
|
||||
return { ok: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle renameCollection command
|
||||
*/
|
||||
private async handleRenameCollection(context: IHandlerContext): Promise<plugins.bson.Document> {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user