This commit is contained in:
2026-01-23 22:15:51 +00:00
commit 74d24cf8b9
44 changed files with 15483 additions and 0 deletions

421
ts/api/handlers.mongodb.ts Normal file
View File

@@ -0,0 +1,421 @@
import * as plugins from '../plugins.js';
import type * as interfaces from '../interfaces/index.js';
import type { TsView } from '../tsview.classes.tsview.js';
/**
* Register MongoDB API handlers
*/
export async function registerMongoHandlers(
typedrouter: plugins.typedrequest.TypedRouter,
tsview: TsView
): Promise<void> {
// Helper to get the native MongoDB client
const getMongoClient = async () => {
const db = await tsview.getMongoDb();
if (!db) {
throw new Error('MongoDB not configured');
}
// Access the underlying MongoDB client through smartdata
return (db as any).mongoDbClient;
};
// Helper to create ObjectId filter
const createIdFilter = (documentId: string) => {
// Try to treat as ObjectId string - MongoDB driver will handle conversion
try {
// Import ObjectId from the mongodb package that smartdata uses
const { ObjectId } = require('mongodb');
if (ObjectId.isValid(documentId)) {
return { _id: new ObjectId(documentId) };
}
} catch {
// Fall through to string filter
}
return { _id: documentId };
};
// List databases
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListDatabases>(
'listDatabases',
async () => {
try {
const client = await getMongoClient();
const adminDb = client.db().admin();
const result = await adminDb.listDatabases();
const databases: interfaces.IMongoDatabase[] = result.databases.map((db: any) => ({
name: db.name,
sizeOnDisk: db.sizeOnDisk,
empty: db.empty,
}));
return { databases };
} catch (err) {
console.error('Error listing databases:', err);
return { databases: [] };
}
}
)
);
// List collections
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListCollections>(
'listCollections',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collectionsInfo = await db.listCollections().toArray();
const collections: interfaces.IMongoCollection[] = [];
for (const coll of collectionsInfo) {
const stats = await db.collection(coll.name).estimatedDocumentCount();
collections.push({
name: coll.name,
count: stats,
});
}
return { collections };
} catch (err) {
console.error('Error listing collections:', err);
return { collections: [] };
}
}
)
);
// Create collection
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_CreateCollection>(
'createCollection',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
await db.createCollection(reqData.collectionName);
return { success: true };
} catch (err) {
console.error('Error creating collection:', err);
return { success: false };
}
}
)
);
// Find documents
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_FindDocuments>(
'findDocuments',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collection = db.collection(reqData.collectionName);
const filter = reqData.filter || {};
const projection = reqData.projection || {};
const sort = reqData.sort || {};
const skip = reqData.skip || 0;
const limit = reqData.limit || 50;
const [documents, total] = await Promise.all([
collection
.find(filter)
.project(projection)
.sort(sort)
.skip(skip)
.limit(limit)
.toArray(),
collection.countDocuments(filter),
]);
// Convert ObjectId to string for JSON serialization
const serializedDocs = documents.map((doc: any) => {
if (doc._id) {
doc._id = doc._id.toString();
}
return doc;
});
return { documents: serializedDocs, total };
} catch (err) {
console.error('Error finding documents:', err);
return { documents: [], total: 0 };
}
}
)
);
// Get single document
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_GetDocument>(
'getDocument',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collection = db.collection(reqData.collectionName);
const filter = createIdFilter(reqData.documentId);
const document = await collection.findOne(filter);
if (document && document._id) {
document._id = document._id.toString();
}
return { document };
} catch (err) {
console.error('Error getting document:', err);
return { document: null };
}
}
)
);
// Insert document
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_InsertDocument>(
'insertDocument',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collection = db.collection(reqData.collectionName);
const result = await collection.insertOne(reqData.document);
return { insertedId: result.insertedId.toString() };
} catch (err) {
console.error('Error inserting document:', err);
throw err;
}
}
)
);
// Update document
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_UpdateDocument>(
'updateDocument',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collection = db.collection(reqData.collectionName);
const filter = createIdFilter(reqData.documentId);
// Check if update has $ operators
const hasOperators = Object.keys(reqData.update).some(k => k.startsWith('$'));
const updateDoc = hasOperators ? reqData.update : { $set: reqData.update };
const result = await collection.updateOne(filter, updateDoc);
return {
success: result.modifiedCount > 0 || result.matchedCount > 0,
modifiedCount: result.modifiedCount,
};
} catch (err) {
console.error('Error updating document:', err);
return { success: false, modifiedCount: 0 };
}
}
)
);
// Delete document
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_DeleteDocument>(
'deleteDocument',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collection = db.collection(reqData.collectionName);
const filter = createIdFilter(reqData.documentId);
const result = await collection.deleteOne(filter);
return {
success: result.deletedCount > 0,
deletedCount: result.deletedCount,
};
} catch (err) {
console.error('Error deleting document:', err);
return { success: false, deletedCount: 0 };
}
}
)
);
// Run aggregation pipeline
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_RunAggregation>(
'runAggregation',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collection = db.collection(reqData.collectionName);
const results = await collection.aggregate(reqData.pipeline).toArray();
// Convert ObjectIds to strings
const serializedResults = results.map((doc: any) => {
if (doc._id) {
doc._id = doc._id.toString();
}
return doc;
});
return { results: serializedResults };
} catch (err) {
console.error('Error running aggregation:', err);
return { results: [] };
}
}
)
);
// List indexes
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListIndexes>(
'listIndexes',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collection = db.collection(reqData.collectionName);
const indexesInfo = await collection.indexes();
const indexes: interfaces.IMongoIndex[] = indexesInfo.map((idx: any) => ({
name: idx.name,
keys: idx.key,
unique: idx.unique || false,
sparse: idx.sparse || false,
}));
return { indexes };
} catch (err) {
console.error('Error listing indexes:', err);
return { indexes: [] };
}
}
)
);
// Create index
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_CreateIndex>(
'createIndex',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collection = db.collection(reqData.collectionName);
const indexName = await collection.createIndex(reqData.keys, reqData.options || {});
return { indexName };
} catch (err) {
console.error('Error creating index:', err);
throw err;
}
}
)
);
// Drop index
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_DropIndex>(
'dropIndex',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collection = db.collection(reqData.collectionName);
await collection.dropIndex(reqData.indexName);
return { success: true };
} catch (err) {
console.error('Error dropping index:', err);
return { success: false };
}
}
)
);
// Get collection stats
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_GetCollectionStats>(
'getCollectionStats',
async (reqData) => {
try {
const client = await getMongoClient();
const db = client.db(reqData.databaseName);
const collection = db.collection(reqData.collectionName);
const stats = await db.command({ collStats: reqData.collectionName });
const indexCount = (await collection.indexes()).length;
return {
stats: {
count: stats.count || 0,
size: stats.size || 0,
avgObjSize: stats.avgObjSize || 0,
storageSize: stats.storageSize || 0,
indexCount,
},
};
} catch (err) {
console.error('Error getting collection stats:', err);
return {
stats: {
count: 0,
size: 0,
avgObjSize: 0,
storageSize: 0,
indexCount: 0,
},
};
}
}
)
);
// Get server status
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_GetServerStatus>(
'getServerStatus',
async () => {
try {
const client = await getMongoClient();
const adminDb = client.db().admin();
const serverInfo = await adminDb.serverInfo();
const serverStatus = await adminDb.serverStatus();
return {
version: serverInfo.version || 'unknown',
uptime: serverStatus.uptime || 0,
connections: {
current: serverStatus.connections?.current || 0,
available: serverStatus.connections?.available || 0,
},
};
} catch (err) {
console.error('Error getting server status:', err);
return {
version: 'unknown',
uptime: 0,
connections: { current: 0, available: 0 },
};
}
}
)
);
}

326
ts/api/handlers.s3.ts Normal file
View File

@@ -0,0 +1,326 @@
import * as plugins from '../plugins.js';
import type * as interfaces from '../interfaces/index.js';
import type { TsView } from '../tsview.classes.tsview.js';
/**
* Register S3 API handlers
*/
export async function registerS3Handlers(
typedrouter: plugins.typedrequest.TypedRouter,
tsview: TsView
): Promise<void> {
// List all buckets
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListBuckets>(
'listBuckets',
async () => {
const smartbucket = await tsview.getSmartBucket();
if (!smartbucket) {
return { buckets: [] };
}
// SmartBucket doesn't have a direct listBuckets method
// For now return empty - in a full implementation you'd use the underlying S3 client
return { buckets: [] };
}
)
);
// Create bucket
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_CreateBucket>(
'createBucket',
async (reqData) => {
const smartbucket = await tsview.getSmartBucket();
if (!smartbucket) {
return { success: false };
}
try {
await smartbucket.createBucket(reqData.bucketName);
return { success: true };
} catch (err) {
console.error('Error creating bucket:', err);
return { success: false };
}
}
)
);
// Delete bucket
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_DeleteBucket>(
'deleteBucket',
async (reqData) => {
const smartbucket = await tsview.getSmartBucket();
if (!smartbucket) {
return { success: false };
}
try {
await smartbucket.removeBucket(reqData.bucketName);
return { success: true };
} catch (err) {
console.error('Error deleting bucket:', err);
return { success: false };
}
}
)
);
// List objects in bucket
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_ListObjects>(
'listObjects',
async (reqData) => {
const smartbucket = await tsview.getSmartBucket();
if (!smartbucket) {
return { objects: [], prefixes: [] };
}
try {
const bucket = await smartbucket.getBucketByName(reqData.bucketName);
if (!bucket) {
return { objects: [], prefixes: [] };
}
const prefix = reqData.prefix || '';
const delimiter = reqData.delimiter || '/';
// Get the base directory or subdirectory
const baseDir = await bucket.getBaseDirectory();
let targetDir = baseDir;
if (prefix) {
// Navigate to the prefix directory
const prefixParts = prefix.replace(/\/$/, '').split('/').filter(Boolean);
for (const part of prefixParts) {
const subDir = await targetDir.getSubDirectoryByName(part, { getEmptyDirectory: true });
if (subDir) {
targetDir = subDir;
} else {
return { objects: [], prefixes: [] };
}
}
}
const objects: interfaces.IS3Object[] = [];
const prefixSet = new Set<string>();
// List files in current directory
const files = await targetDir.listFiles();
for (const file of files) {
const fullPath = prefix + file.name;
objects.push({
key: fullPath,
isPrefix: false,
});
}
// List subdirectories
const dirs = await targetDir.listDirectories();
for (const dir of dirs) {
const fullPrefix = prefix + dir.name + '/';
prefixSet.add(fullPrefix);
}
return {
objects,
prefixes: Array.from(prefixSet),
};
} catch (err) {
console.error('Error listing objects:', err);
return { objects: [], prefixes: [] };
}
}
)
);
// Get object content
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_GetObject>(
'getObject',
async (reqData) => {
const smartbucket = await tsview.getSmartBucket();
if (!smartbucket) {
throw new Error('S3 not configured');
}
try {
const bucket = await smartbucket.getBucketByName(reqData.bucketName);
if (!bucket) {
throw new Error(`Bucket ${reqData.bucketName} not found`);
}
const content = await bucket.fastGet({ path: reqData.key });
const stats = await bucket.fastStat({ path: reqData.key });
// Determine content type from extension
const ext = reqData.key.split('.').pop()?.toLowerCase() || '';
const contentTypeMap: Record<string, string> = {
'json': 'application/json',
'txt': 'text/plain',
'html': 'text/html',
'css': 'text/css',
'js': 'application/javascript',
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'svg': 'image/svg+xml',
'pdf': 'application/pdf',
'xml': 'application/xml',
};
const contentType = contentTypeMap[ext] || 'application/octet-stream';
return {
content: content.toString('base64'),
contentType,
size: stats?.ContentLength || content.length,
lastModified: stats?.LastModified?.toISOString() || new Date().toISOString(),
};
} catch (err) {
console.error('Error getting object:', err);
throw err;
}
}
)
);
// Get object metadata
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_GetObjectMetadata>(
'getObjectMetadata',
async (reqData) => {
const smartbucket = await tsview.getSmartBucket();
if (!smartbucket) {
throw new Error('S3 not configured');
}
try {
const bucket = await smartbucket.getBucketByName(reqData.bucketName);
if (!bucket) {
throw new Error(`Bucket ${reqData.bucketName} not found`);
}
const stats = await bucket.fastStat({ path: reqData.key });
const ext = reqData.key.split('.').pop()?.toLowerCase() || '';
const contentTypeMap: Record<string, string> = {
'json': 'application/json',
'txt': 'text/plain',
'html': 'text/html',
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'pdf': 'application/pdf',
};
const contentType = contentTypeMap[ext] || 'application/octet-stream';
return {
contentType,
size: stats?.ContentLength || 0,
lastModified: stats?.LastModified?.toISOString() || new Date().toISOString(),
};
} catch (err) {
console.error('Error getting object metadata:', err);
throw err;
}
}
)
);
// Put object
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_PutObject>(
'putObject',
async (reqData) => {
const smartbucket = await tsview.getSmartBucket();
if (!smartbucket) {
return { success: false };
}
try {
const bucket = await smartbucket.getBucketByName(reqData.bucketName);
if (!bucket) {
return { success: false };
}
const content = Buffer.from(reqData.content, 'base64');
await bucket.fastPut({
path: reqData.key,
contents: content,
});
return { success: true };
} catch (err) {
console.error('Error putting object:', err);
return { success: false };
}
}
)
);
// Delete object
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_DeleteObject>(
'deleteObject',
async (reqData) => {
const smartbucket = await tsview.getSmartBucket();
if (!smartbucket) {
return { success: false };
}
try {
const bucket = await smartbucket.getBucketByName(reqData.bucketName);
if (!bucket) {
return { success: false };
}
await bucket.fastRemove({ path: reqData.key });
return { success: true };
} catch (err) {
console.error('Error deleting object:', err);
return { success: false };
}
}
)
);
// Copy object
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_CopyObject>(
'copyObject',
async (reqData) => {
const smartbucket = await tsview.getSmartBucket();
if (!smartbucket) {
return { success: false };
}
try {
const sourceBucket = await smartbucket.getBucketByName(reqData.sourceBucket);
const destBucket = await smartbucket.getBucketByName(reqData.destBucket);
if (!sourceBucket || !destBucket) {
return { success: false };
}
// Read from source
const content = await sourceBucket.fastGet({ path: reqData.sourceKey });
// Write to destination
await destBucket.fastPut({
path: reqData.destKey,
contents: content,
});
return { success: true };
} catch (err) {
console.error('Error copying object:', err);
return { success: false };
}
}
)
);
}

2
ts/api/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './handlers.s3.js';
export * from './handlers.mongodb.js';