initial
This commit is contained in:
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitance data by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsview',
|
||||
version: '1.0.0',
|
||||
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI',
|
||||
};
|
||||
421
ts/api/handlers.mongodb.ts
Normal file
421
ts/api/handlers.mongodb.ts
Normal 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
326
ts/api/handlers.s3.ts
Normal 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
2
ts/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './handlers.s3.js';
|
||||
export * from './handlers.mongodb.js';
|
||||
11
ts/bundled_ui.ts
Normal file
11
ts/bundled_ui.ts
Normal file
File diff suppressed because one or more lines are too long
110
ts/config/classes.config.ts
Normal file
110
ts/config/classes.config.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type * as interfaces from '../interfaces/index.js';
|
||||
|
||||
/**
|
||||
* Configuration manager for tsview.
|
||||
* Reads configuration from .nogit/env.json (gitzone service format)
|
||||
* or accepts programmatic configuration.
|
||||
*/
|
||||
export class TsViewConfig {
|
||||
private s3Config: interfaces.IS3Config | null = null;
|
||||
private mongoConfig: interfaces.IMongoConfig | null = null;
|
||||
|
||||
/**
|
||||
* Load configuration from .nogit/env.json
|
||||
* @param cwd - Working directory (defaults to process.cwd())
|
||||
*/
|
||||
public async loadFromEnv(cwd: string = process.cwd()): Promise<void> {
|
||||
const envPath = plugins.path.join(cwd, '.nogit', 'env.json');
|
||||
|
||||
try {
|
||||
const factory = plugins.smartfile.SmartFileFactory.nodeFs();
|
||||
const envFile = await factory.fromFilePath(envPath);
|
||||
var envContent = envFile.parseContentAsString();
|
||||
} catch (err) {
|
||||
console.log(`No .nogit/env.json found at ${envPath}`);
|
||||
return;
|
||||
}
|
||||
const envConfig: interfaces.IEnvConfig = JSON.parse(envContent);
|
||||
|
||||
// Parse S3 config
|
||||
if (envConfig.S3_HOST || envConfig.S3_ENDPOINT) {
|
||||
this.s3Config = {
|
||||
endpoint: envConfig.S3_ENDPOINT || envConfig.S3_HOST || '',
|
||||
port: envConfig.S3_PORT ? parseInt(envConfig.S3_PORT, 10) : undefined,
|
||||
accessKey: envConfig.S3_ACCESSKEY || '',
|
||||
accessSecret: envConfig.S3_SECRETKEY || '',
|
||||
useSsl: envConfig.S3_USESSL === true || envConfig.S3_USESSL === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
// Parse MongoDB config
|
||||
if (envConfig.MONGODB_URL) {
|
||||
this.mongoConfig = {
|
||||
mongoDbUrl: envConfig.MONGODB_URL,
|
||||
mongoDbName: envConfig.MONGODB_NAME || 'test',
|
||||
};
|
||||
} else if (envConfig.MONGODB_HOST) {
|
||||
// Build URL from parts
|
||||
const host = envConfig.MONGODB_HOST;
|
||||
const port = envConfig.MONGODB_PORT || '27017';
|
||||
const user = envConfig.MONGODB_USER;
|
||||
const pass = envConfig.MONGODB_PASS;
|
||||
const dbName = envConfig.MONGODB_NAME || 'test';
|
||||
|
||||
let url: string;
|
||||
if (user && pass) {
|
||||
url = `mongodb://${encodeURIComponent(user)}:${encodeURIComponent(pass)}@${host}:${port}`;
|
||||
} else {
|
||||
url = `mongodb://${host}:${port}`;
|
||||
}
|
||||
|
||||
this.mongoConfig = {
|
||||
mongoDbUrl: url,
|
||||
mongoDbName: dbName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set S3 configuration programmatically
|
||||
*/
|
||||
public setS3Config(config: interfaces.IS3Config): void {
|
||||
this.s3Config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set MongoDB configuration programmatically
|
||||
*/
|
||||
public setMongoConfig(config: interfaces.IMongoConfig): void {
|
||||
this.mongoConfig = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get S3 configuration
|
||||
*/
|
||||
public getS3Config(): interfaces.IS3Config | null {
|
||||
return this.s3Config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MongoDB configuration
|
||||
*/
|
||||
public getMongoConfig(): interfaces.IMongoConfig | null {
|
||||
return this.mongoConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if S3 is configured
|
||||
*/
|
||||
public hasS3(): boolean {
|
||||
return this.s3Config !== null && !!this.s3Config.endpoint && !!this.s3Config.accessKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if MongoDB is configured
|
||||
*/
|
||||
public hasMongo(): boolean {
|
||||
return this.mongoConfig !== null && !!this.mongoConfig.mongoDbUrl;
|
||||
}
|
||||
}
|
||||
1
ts/config/index.ts
Normal file
1
ts/config/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './classes.config.js';
|
||||
12
ts/index.ts
Normal file
12
ts/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
// Export main classes
|
||||
export { TsView } from './tsview.classes.tsview.js';
|
||||
export * from './interfaces/index.js';
|
||||
|
||||
// CLI entry point
|
||||
export const runCli = async () => {
|
||||
const { TsViewCli } = await import('./tsview.cli.js');
|
||||
const cli = new TsViewCli();
|
||||
await cli.run();
|
||||
};
|
||||
436
ts/interfaces/index.ts
Normal file
436
ts/interfaces/index.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
import type * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Configuration for S3 connection
|
||||
*/
|
||||
export interface IS3Config {
|
||||
endpoint: string;
|
||||
port?: number;
|
||||
accessKey: string;
|
||||
accessSecret: string;
|
||||
useSsl?: boolean;
|
||||
region?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for MongoDB connection
|
||||
*/
|
||||
export interface IMongoConfig {
|
||||
mongoDbUrl: string;
|
||||
mongoDbName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined configuration for tsview
|
||||
*/
|
||||
export interface ITsViewConfig {
|
||||
s3?: IS3Config;
|
||||
mongo?: IMongoConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment configuration from .nogit/env.json (gitzone service format)
|
||||
*/
|
||||
export interface IEnvConfig {
|
||||
MONGODB_URL?: string;
|
||||
MONGODB_HOST?: string;
|
||||
MONGODB_PORT?: string;
|
||||
MONGODB_USER?: string;
|
||||
MONGODB_PASS?: string;
|
||||
MONGODB_NAME?: string;
|
||||
S3_HOST?: string;
|
||||
S3_PORT?: string;
|
||||
S3_ACCESSKEY?: string;
|
||||
S3_SECRETKEY?: string;
|
||||
S3_BUCKET?: string;
|
||||
S3_ENDPOINT?: string;
|
||||
S3_USESSL?: boolean | string;
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// TypedRequest interfaces for S3 API
|
||||
// ===========================================
|
||||
|
||||
export interface IReq_ListBuckets extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_ListBuckets
|
||||
> {
|
||||
method: 'listBuckets';
|
||||
request: {};
|
||||
response: {
|
||||
buckets: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_CreateBucket extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_CreateBucket
|
||||
> {
|
||||
method: 'createBucket';
|
||||
request: {
|
||||
bucketName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_DeleteBucket extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_DeleteBucket
|
||||
> {
|
||||
method: 'deleteBucket';
|
||||
request: {
|
||||
bucketName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IS3Object {
|
||||
key: string;
|
||||
size?: number;
|
||||
lastModified?: string;
|
||||
isPrefix?: boolean;
|
||||
}
|
||||
|
||||
export interface IReq_ListObjects extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_ListObjects
|
||||
> {
|
||||
method: 'listObjects';
|
||||
request: {
|
||||
bucketName: string;
|
||||
prefix?: string;
|
||||
delimiter?: string;
|
||||
};
|
||||
response: {
|
||||
objects: IS3Object[];
|
||||
prefixes: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_GetObject extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetObject
|
||||
> {
|
||||
method: 'getObject';
|
||||
request: {
|
||||
bucketName: string;
|
||||
key: string;
|
||||
};
|
||||
response: {
|
||||
content: string; // base64
|
||||
contentType: string;
|
||||
size: number;
|
||||
lastModified: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_GetObjectMetadata extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetObjectMetadata
|
||||
> {
|
||||
method: 'getObjectMetadata';
|
||||
request: {
|
||||
bucketName: string;
|
||||
key: string;
|
||||
};
|
||||
response: {
|
||||
contentType: string;
|
||||
size: number;
|
||||
lastModified: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_PutObject extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_PutObject
|
||||
> {
|
||||
method: 'putObject';
|
||||
request: {
|
||||
bucketName: string;
|
||||
key: string;
|
||||
content: string; // base64
|
||||
contentType: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_DeleteObject extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_DeleteObject
|
||||
> {
|
||||
method: 'deleteObject';
|
||||
request: {
|
||||
bucketName: string;
|
||||
key: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_CopyObject extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_CopyObject
|
||||
> {
|
||||
method: 'copyObject';
|
||||
request: {
|
||||
sourceBucket: string;
|
||||
sourceKey: string;
|
||||
destBucket: string;
|
||||
destKey: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// TypedRequest interfaces for MongoDB API
|
||||
// ===========================================
|
||||
|
||||
export interface IMongoDatabase {
|
||||
name: string;
|
||||
sizeOnDisk?: number;
|
||||
empty?: boolean;
|
||||
}
|
||||
|
||||
export interface IReq_ListDatabases extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_ListDatabases
|
||||
> {
|
||||
method: 'listDatabases';
|
||||
request: {};
|
||||
response: {
|
||||
databases: IMongoDatabase[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IMongoCollection {
|
||||
name: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface IReq_ListCollections extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_ListCollections
|
||||
> {
|
||||
method: 'listCollections';
|
||||
request: {
|
||||
databaseName: string;
|
||||
};
|
||||
response: {
|
||||
collections: IMongoCollection[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_CreateCollection extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_CreateCollection
|
||||
> {
|
||||
method: 'createCollection';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_FindDocuments extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_FindDocuments
|
||||
> {
|
||||
method: 'findDocuments';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
filter?: Record<string, unknown>;
|
||||
projection?: Record<string, unknown>;
|
||||
sort?: Record<string, number>;
|
||||
skip?: number;
|
||||
limit?: number;
|
||||
};
|
||||
response: {
|
||||
documents: Record<string, unknown>[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_GetDocument extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetDocument
|
||||
> {
|
||||
method: 'getDocument';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
documentId: string;
|
||||
};
|
||||
response: {
|
||||
document: Record<string, unknown> | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_InsertDocument extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_InsertDocument
|
||||
> {
|
||||
method: 'insertDocument';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
document: Record<string, unknown>;
|
||||
};
|
||||
response: {
|
||||
insertedId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_UpdateDocument extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_UpdateDocument
|
||||
> {
|
||||
method: 'updateDocument';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
documentId: string;
|
||||
update: Record<string, unknown>;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
modifiedCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_DeleteDocument extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_DeleteDocument
|
||||
> {
|
||||
method: 'deleteDocument';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
documentId: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
deletedCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_RunAggregation extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_RunAggregation
|
||||
> {
|
||||
method: 'runAggregation';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
pipeline: Record<string, unknown>[];
|
||||
};
|
||||
response: {
|
||||
results: Record<string, unknown>[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IMongoIndex {
|
||||
name: string;
|
||||
keys: Record<string, number>;
|
||||
unique?: boolean;
|
||||
sparse?: boolean;
|
||||
}
|
||||
|
||||
export interface IReq_ListIndexes extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_ListIndexes
|
||||
> {
|
||||
method: 'listIndexes';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
};
|
||||
response: {
|
||||
indexes: IMongoIndex[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_CreateIndex extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_CreateIndex
|
||||
> {
|
||||
method: 'createIndex';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
keys: Record<string, number>;
|
||||
options?: {
|
||||
unique?: boolean;
|
||||
sparse?: boolean;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
response: {
|
||||
indexName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_DropIndex extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_DropIndex
|
||||
> {
|
||||
method: 'dropIndex';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
indexName: string;
|
||||
};
|
||||
response: {
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICollectionStats {
|
||||
count: number;
|
||||
size: number;
|
||||
avgObjSize: number;
|
||||
storageSize: number;
|
||||
indexCount: number;
|
||||
}
|
||||
|
||||
export interface IReq_GetCollectionStats extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetCollectionStats
|
||||
> {
|
||||
method: 'getCollectionStats';
|
||||
request: {
|
||||
databaseName: string;
|
||||
collectionName: string;
|
||||
};
|
||||
response: {
|
||||
stats: ICollectionStats;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReq_GetServerStatus extends plugins.typedrequestInterfaces.implementsTR<
|
||||
plugins.typedrequestInterfaces.ITypedRequest,
|
||||
IReq_GetServerStatus
|
||||
> {
|
||||
method: 'getServerStatus';
|
||||
request: {};
|
||||
response: {
|
||||
version: string;
|
||||
uptime: number;
|
||||
connections: {
|
||||
current: number;
|
||||
available: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
8
ts/paths.ts
Normal file
8
ts/paths.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export const packageDir = plugins.path.resolve(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'..'
|
||||
);
|
||||
export const tsDir = plugins.path.join(packageDir, 'ts');
|
||||
export const distTsDir = plugins.path.join(packageDir, 'dist_ts');
|
||||
45
ts/plugins.ts
Normal file
45
ts/plugins.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// node native
|
||||
import * as path from 'path';
|
||||
export { path };
|
||||
|
||||
// @push.rocks scope
|
||||
import * as early from '@push.rocks/early';
|
||||
early.start('tsview');
|
||||
|
||||
import * as npmextra from '@push.rocks/npmextra';
|
||||
import * as smartbucket from '@push.rocks/smartbucket';
|
||||
import * as smartcli from '@push.rocks/smartcli';
|
||||
import * as smartdata from '@push.rocks/smartdata';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
||||
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||
import * as smartopen from '@push.rocks/smartopen';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
|
||||
export {
|
||||
early,
|
||||
npmextra,
|
||||
smartbucket,
|
||||
smartcli,
|
||||
smartdata,
|
||||
smartfile,
|
||||
smartlog,
|
||||
smartlogDestinationLocal,
|
||||
smartnetwork,
|
||||
smartopen,
|
||||
smartpath,
|
||||
smartpromise,
|
||||
};
|
||||
|
||||
// @api.global scope
|
||||
import * as typedrequest from '@api.global/typedrequest';
|
||||
import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||
import * as typedserver from '@api.global/typedserver';
|
||||
|
||||
export {
|
||||
typedrequest,
|
||||
typedrequestInterfaces,
|
||||
typedserver,
|
||||
};
|
||||
59
ts/server/classes.viewserver.ts
Normal file
59
ts/server/classes.viewserver.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { TsView } from '../tsview.classes.tsview.js';
|
||||
import { registerS3Handlers } from '../api/handlers.s3.js';
|
||||
import { registerMongoHandlers } from '../api/handlers.mongodb.js';
|
||||
import { files as bundledUiFiles } from '../bundled_ui.js';
|
||||
|
||||
/**
|
||||
* Web server for TsView that serves the bundled UI and API endpoints.
|
||||
*/
|
||||
export class ViewServer {
|
||||
private tsview: TsView;
|
||||
private port: number;
|
||||
private typedServer: plugins.typedserver.TypedServer | null = null;
|
||||
public typedrouter: plugins.typedrequest.TypedRouter;
|
||||
|
||||
constructor(tsview: TsView, port: number) {
|
||||
this.tsview = tsview;
|
||||
this.port = port;
|
||||
this.typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
// Register API handlers
|
||||
if (this.tsview.config.hasS3()) {
|
||||
await registerS3Handlers(this.typedrouter, this.tsview);
|
||||
}
|
||||
|
||||
if (this.tsview.config.hasMongo()) {
|
||||
await registerMongoHandlers(this.typedrouter, this.tsview);
|
||||
}
|
||||
|
||||
// Create typed server with bundled content
|
||||
this.typedServer = new plugins.typedserver.TypedServer({
|
||||
cors: true,
|
||||
port: this.port,
|
||||
bundledContent: bundledUiFiles,
|
||||
spaFallback: true,
|
||||
});
|
||||
|
||||
// Add the router
|
||||
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
||||
|
||||
// Start server
|
||||
await this.typedServer.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
if (this.typedServer) {
|
||||
await this.typedServer.stop();
|
||||
this.typedServer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
ts/server/index.ts
Normal file
1
ts/server/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './classes.viewserver.js';
|
||||
137
ts/tsview.classes.tsview.ts
Normal file
137
ts/tsview.classes.tsview.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import type * as interfaces from './interfaces/index.js';
|
||||
import { TsViewConfig } from './config/index.js';
|
||||
import { ViewServer } from './server/index.js';
|
||||
|
||||
/**
|
||||
* Main TsView class.
|
||||
* Provides both CLI and programmatic access to S3 and MongoDB viewing.
|
||||
*/
|
||||
export class TsView {
|
||||
public config: TsViewConfig;
|
||||
public server: ViewServer | null = null;
|
||||
|
||||
private smartbucketInstance: plugins.smartbucket.SmartBucket | null = null;
|
||||
private mongoDbConnection: plugins.smartdata.SmartdataDb | null = null;
|
||||
|
||||
constructor() {
|
||||
this.config = new TsViewConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from .nogit/env.json
|
||||
*/
|
||||
public async loadConfigFromEnv(cwd?: string): Promise<void> {
|
||||
await this.config.loadFromEnv(cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set S3 configuration programmatically
|
||||
*/
|
||||
public setS3Config(config: interfaces.IS3Config): void {
|
||||
this.config.setS3Config(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set MongoDB configuration programmatically
|
||||
*/
|
||||
public setMongoConfig(config: interfaces.IMongoConfig): void {
|
||||
this.config.setMongoConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SmartBucket instance (lazy initialization)
|
||||
*/
|
||||
public async getSmartBucket(): Promise<plugins.smartbucket.SmartBucket | null> {
|
||||
if (this.smartbucketInstance) {
|
||||
return this.smartbucketInstance;
|
||||
}
|
||||
|
||||
const s3Config = this.config.getS3Config();
|
||||
if (!s3Config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.smartbucketInstance = new plugins.smartbucket.SmartBucket({
|
||||
endpoint: s3Config.endpoint,
|
||||
port: s3Config.port,
|
||||
accessKey: s3Config.accessKey,
|
||||
accessSecret: s3Config.accessSecret,
|
||||
useSsl: s3Config.useSsl ?? true,
|
||||
});
|
||||
|
||||
return this.smartbucketInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MongoDB connection (lazy initialization)
|
||||
*/
|
||||
public async getMongoDb(): Promise<plugins.smartdata.SmartdataDb | null> {
|
||||
if (this.mongoDbConnection) {
|
||||
return this.mongoDbConnection;
|
||||
}
|
||||
|
||||
const mongoConfig = this.config.getMongoConfig();
|
||||
if (!mongoConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.mongoDbConnection = new plugins.smartdata.SmartdataDb({
|
||||
mongoDbUrl: mongoConfig.mongoDbUrl,
|
||||
mongoDbName: mongoConfig.mongoDbName,
|
||||
});
|
||||
|
||||
await this.mongoDbConnection.init();
|
||||
return this.mongoDbConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a free port starting from the given port
|
||||
*/
|
||||
private async findFreePort(startPort: number = 3010): Promise<number> {
|
||||
const network = new plugins.smartnetwork.SmartNetwork();
|
||||
const freePort = await network.findFreePort(startPort, startPort + 100);
|
||||
if (freePort === null) {
|
||||
throw new Error(`No free port found between ${startPort} and ${startPort + 100}`);
|
||||
}
|
||||
return freePort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the viewer server
|
||||
* @param port - Optional port number (if not provided, finds a free port from 3010+)
|
||||
*/
|
||||
public async start(port?: number): Promise<number> {
|
||||
const actualPort = port ?? await this.findFreePort(3010);
|
||||
|
||||
this.server = new ViewServer(this, actualPort);
|
||||
await this.server.start();
|
||||
|
||||
console.log(`TsView server started on http://localhost:${actualPort}`);
|
||||
|
||||
// Open browser
|
||||
try {
|
||||
await plugins.smartopen.openUrl(`http://localhost:${actualPort}`);
|
||||
} catch (err) {
|
||||
// Ignore browser open errors
|
||||
}
|
||||
|
||||
return actualPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the viewer server
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
if (this.server) {
|
||||
await this.server.stop();
|
||||
this.server = null;
|
||||
}
|
||||
|
||||
if (this.mongoDbConnection) {
|
||||
await this.mongoDbConnection.close();
|
||||
this.mongoDbConnection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
127
ts/tsview.cli.ts
Normal file
127
ts/tsview.cli.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { TsView } from './tsview.classes.tsview.js';
|
||||
|
||||
/**
|
||||
* CLI handler for tsview
|
||||
*/
|
||||
export class TsViewCli {
|
||||
private smartcli: plugins.smartcli.Smartcli;
|
||||
|
||||
constructor() {
|
||||
this.smartcli = new plugins.smartcli.Smartcli();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the CLI
|
||||
*/
|
||||
public async run(): Promise<void> {
|
||||
// Default command (no arguments)
|
||||
this.smartcli.standardCommand().subscribe(async (argvArg) => {
|
||||
await this.startViewer({
|
||||
port: argvArg.port as number | undefined,
|
||||
s3Only: false,
|
||||
mongoOnly: false,
|
||||
});
|
||||
});
|
||||
|
||||
// S3-only command
|
||||
const s3Command = this.smartcli.addCommand('s3');
|
||||
s3Command.subscribe(async (argvArg) => {
|
||||
await this.startViewer({
|
||||
port: argvArg.port as number | undefined,
|
||||
s3Only: true,
|
||||
mongoOnly: false,
|
||||
});
|
||||
});
|
||||
|
||||
// MongoDB-only command
|
||||
const mongoCommand = this.smartcli.addCommand('mongo');
|
||||
mongoCommand.subscribe(async (argvArg) => {
|
||||
await this.startViewer({
|
||||
port: argvArg.port as number | undefined,
|
||||
s3Only: false,
|
||||
mongoOnly: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Alias for mongo command
|
||||
this.smartcli.addCommandAlias('mongo', 'mongodb');
|
||||
|
||||
// Start parsing
|
||||
await this.smartcli.startParse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the viewer
|
||||
*/
|
||||
private async startViewer(options: {
|
||||
port?: number;
|
||||
s3Only: boolean;
|
||||
mongoOnly: boolean;
|
||||
}): Promise<void> {
|
||||
console.log('Starting TsView...');
|
||||
|
||||
const viewer = new TsView();
|
||||
|
||||
// Load config from env.json
|
||||
await viewer.loadConfigFromEnv();
|
||||
|
||||
// Check what's configured
|
||||
const hasS3 = viewer.config.hasS3();
|
||||
const hasMongo = viewer.config.hasMongo();
|
||||
|
||||
if (!hasS3 && !hasMongo) {
|
||||
console.error('Error: No S3 or MongoDB configuration found.');
|
||||
console.error('Please create .nogit/env.json with your configuration.');
|
||||
console.error('');
|
||||
console.error('Example .nogit/env.json:');
|
||||
console.error(JSON.stringify({
|
||||
S3_ENDPOINT: 'localhost',
|
||||
S3_PORT: '9000',
|
||||
S3_ACCESSKEY: 'minioadmin',
|
||||
S3_SECRETKEY: 'minioadmin',
|
||||
S3_USESSL: false,
|
||||
MONGODB_URL: 'mongodb://localhost:27017',
|
||||
MONGODB_NAME: 'mydb',
|
||||
}, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.s3Only && !hasS3) {
|
||||
console.error('Error: S3 configuration not found in .nogit/env.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.mongoOnly && !hasMongo) {
|
||||
console.error('Error: MongoDB configuration not found in .nogit/env.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Log what's available
|
||||
if (hasS3) {
|
||||
console.log('S3 storage configured');
|
||||
}
|
||||
if (hasMongo) {
|
||||
console.log('MongoDB configured');
|
||||
}
|
||||
|
||||
// Start the server
|
||||
const port = await viewer.start(options.port);
|
||||
|
||||
// Keep process running
|
||||
console.log(`Press Ctrl+C to stop`);
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\nShutting down...');
|
||||
await viewer.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('\nShutting down...');
|
||||
await viewer.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user