UNPKG

@codai/memorai

Version:

Universal Database & Storage Service for CODAI Ecosystem - CBD Backend

733 lines (617 loc) 21.7 kB
/** * MEMORAI REST API Server * Provides HTTP endpoints for database, storage, memory, and sync operations */ import { EventEmitter } from 'events' import express, { Express, Request, Response, NextFunction } from 'express' import cors from 'cors' import helmet from 'helmet' import rateLimit from 'express-rate-limit' import multer from 'multer' import { MemoraiService } from '../services/MemoraiService' import type { MemoraiConfig } from '../types' interface AuthRequest extends Request { user?: { id: string email: string name: string roles: string[] } tenant?: { id: string name: string } } export class MemoraiAPIServer extends EventEmitter { private app: Express private server?: any private _isRunning = false private config: MemoraiConfig private memoraiService: MemoraiService private upload: multer.Multer constructor(memoraiService: MemoraiService, config: MemoraiConfig) { super() this.memoraiService = memoraiService this.config = config this.app = express() // Configure multer for file uploads this.upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: config.storage.maxFileSize }, fileFilter: (req, file, cb) => { if (config.storage.allowedTypes.length === 0) { return cb(null, true) } const isAllowed = config.storage.allowedTypes.some(type => file.mimetype.startsWith(type) || file.mimetype === type ) if (isAllowed) { cb(null, true) } else { cb(new Error(`File type not allowed: ${file.mimetype}`)) } } }) this.setupMiddleware() this.setupRoutes() this.setupErrorHandling() } // ==================== SERVER LIFECYCLE ==================== async start(port = 3001, host = 'localhost'): Promise<void> { if (this._isRunning) { throw new Error('API server is already running') } try { // Ensure memorai service is initialized if (!this.memoraiService.isReady) { await this.memoraiService.initialize() } // Start HTTP server this.server = this.app.listen(port, host, () => { this._isRunning = true this.emit('started', { port, host }) console.log(`🌐 MEMORAI API Server started on http://${host}:${port}`) }) } catch (error) { console.error('Failed to start API server:', error) this.emit('error', error) throw error } } async stop(): Promise<void> { if (!this._isRunning) return try { if (this.server) { this.server.close(() => { this._isRunning = false this.emit('stopped') console.log('🌐 MEMORAI API Server stopped') }) } } catch (error) { console.error('Error stopping API server:', error) this.emit('error', error) throw error } } // ==================== MIDDLEWARE SETUP ==================== private setupMiddleware(): void { // Security middleware this.app.use(helmet()) // CORS this.app.use(cors({ origin: this.config.security.cors.origins, credentials: this.config.security.cors.credentials })) // Rate limiting if (this.config.security.rateLimit.enabled) { this.app.use(rateLimit({ windowMs: this.config.security.rateLimit.windowMs, max: this.config.security.rateLimit.maxRequests, message: 'Too many requests from this IP, please try again later', standardHeaders: true, legacyHeaders: false })) } // Body parsing this.app.use(express.json({ limit: '10mb' })) this.app.use(express.urlencoded({ extended: true, limit: '10mb' })) // Request logging this.app.use((req: Request, res: Response, next: NextFunction) => { console.log(`📥 ${req.method} ${req.path} - ${req.ip}`) next() }) // Authentication middleware (mock for now) this.app.use((req: AuthRequest, res: Response, next: NextFunction) => { // TODO: Implement real authentication with @codai/auth req.user = { id: req.headers['x-user-id'] as string || 'anonymous', email: req.headers['x-user-email'] as string || 'anonymous@example.com', name: req.headers['x-user-name'] as string || 'Anonymous User', roles: ['user'] } req.tenant = { id: req.headers['x-tenant-id'] as string || 'default', name: 'Default Tenant' } next() }) } // ==================== ROUTES SETUP ==================== private setupRoutes(): void { // Health check this.app.get('/health', this.handleHealthCheck.bind(this)) // API info this.app.get('/api/info', this.handleAPIInfo.bind(this)) // Database routes this.app.post('/api/v1/database/:table', this.handleDatabaseCreate.bind(this)) this.app.get('/api/v1/database/:table/:id', this.handleDatabaseGet.bind(this)) this.app.get('/api/v1/database/:table', this.handleDatabaseFind.bind(this)) this.app.put('/api/v1/database/:table/:id', this.handleDatabaseUpdate.bind(this)) this.app.delete('/api/v1/database/:table/:id', this.handleDatabaseDelete.bind(this)) this.app.post('/api/v1/database/query', this.handleDatabaseQuery.bind(this)) // Storage routes this.app.post('/api/v1/storage/upload', this.upload.single('file'), this.handleStorageUpload.bind(this)) this.app.get('/api/v1/storage/:path(*)', this.handleStorageDownload.bind(this)) this.app.delete('/api/v1/storage/:path(*)', this.handleStorageDelete.bind(this)) this.app.get('/api/v1/storage', this.handleStorageList.bind(this)) // Memory routes this.app.post('/api/v1/memory', this.handleMemoryStore.bind(this)) this.app.post('/api/v1/memory/search', this.handleMemorySearch.bind(this)) this.app.get('/api/v1/memory/:id', this.handleMemoryGet.bind(this)) this.app.put('/api/v1/memory/:id', this.handleMemoryUpdate.bind(this)) this.app.delete('/api/v1/memory/:id', this.handleMemoryDelete.bind(this)) // Analytics routes this.app.post('/api/v1/analytics/track', this.handleAnalyticsTrack.bind(this)) this.app.post('/api/v1/analytics/query', this.handleAnalyticsQuery.bind(this)) // Cache routes this.app.get('/api/v1/cache/:key', this.handleCacheGet.bind(this)) this.app.put('/api/v1/cache/:key', this.handleCacheSet.bind(this)) this.app.delete('/api/v1/cache/:key', this.handleCacheDelete.bind(this)) // Sync routes this.app.get('/api/v1/sync/status', this.handleSyncStatus.bind(this)) this.app.post('/api/v1/sync/resolve-conflict', this.handleSyncResolveConflict.bind(this)) } // ==================== ROUTE HANDLERS ==================== private async handleHealthCheck(req: Request, res: Response): Promise<void> { try { const health = await this.memoraiService.getHealth() res.json(health) } catch (error) { res.status(500).json({ status: 'error', error: error instanceof Error ? error.message : 'Unknown error' }) } } private async handleAPIInfo(req: Request, res: Response): Promise<void> { res.json({ name: 'MEMORAI API', version: '1.0.0', description: 'Universal Database & Storage Service for CODAI Ecosystem', endpoints: { database: [ 'POST /api/v1/database/:table', 'GET /api/v1/database/:table/:id', 'GET /api/v1/database/:table', 'PUT /api/v1/database/:table/:id', 'DELETE /api/v1/database/:table/:id', 'POST /api/v1/database/query' ], storage: [ 'POST /api/v1/storage/upload', 'GET /api/v1/storage/:path', 'DELETE /api/v1/storage/:path', 'GET /api/v1/storage' ], memory: [ 'POST /api/v1/memory', 'POST /api/v1/memory/search', 'GET /api/v1/memory/:id', 'PUT /api/v1/memory/:id', 'DELETE /api/v1/memory/:id' ], analytics: [ 'POST /api/v1/analytics/track', 'POST /api/v1/analytics/query' ], cache: [ 'GET /api/v1/cache/:key', 'PUT /api/v1/cache/:key', 'DELETE /api/v1/cache/:key' ], sync: [ 'GET /api/v1/sync/status', 'POST /api/v1/sync/resolve-conflict' ] } }) } // Database handlers private async handleDatabaseCreate(req: AuthRequest, res: Response): Promise<void> { try { const { table } = req.params const data = req.body const result = await this.memoraiService.insert(table, data) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Database create failed' }) } } private async handleDatabaseGet(req: AuthRequest, res: Response): Promise<void> { try { const { table, id } = req.params const result = await this.memoraiService.find(table, [ { field: 'id', operator: '=', value: id } ]) if (result.data && result.data.length > 0) { res.json({ success: true, data: result.data[0] }) } else { res.status(404).json({ success: false, error: 'Record not found' }) } } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Database get failed' }) } } private async handleDatabaseFind(req: AuthRequest, res: Response): Promise<void> { try { const { table } = req.params const { limit, offset, orderBy, ...filters } = req.query // Build conditions from query parameters const conditions = Object.entries(filters).map(([field, value]) => ({ field, operator: '=' as const, value })) const options: any = {} if (limit) options.limit = parseInt(limit as string) if (offset) options.offset = parseInt(offset as string) if (orderBy) { options.orderBy = [{ field: orderBy as string, direction: 'ASC' }] } const result = await this.memoraiService.find(table, conditions, options) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Database find failed' }) } } private async handleDatabaseUpdate(req: AuthRequest, res: Response): Promise<void> { try { const { table, id } = req.params const data = req.body const result = await this.memoraiService.update(table, id, data) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Database update failed' }) } } private async handleDatabaseDelete(req: AuthRequest, res: Response): Promise<void> { try { const { table, id } = req.params const result = await this.memoraiService.delete(table, id) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Database delete failed' }) } } private async handleDatabaseQuery(req: AuthRequest, res: Response): Promise<void> { try { const query = req.body const result = await this.memoraiService.query(query) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Database query failed' }) } } // Storage handlers private async handleStorageUpload(req: AuthRequest, res: Response): Promise<void> { try { if (!req.file) { res.status(400).json({ success: false, error: 'No file provided' }) return } const storageUpload = { file: req.file.buffer, filename: req.file.originalname, mimeType: req.file.mimetype, size: req.file.size, isPublic: req.body.isPublic === 'true', tags: req.body.tags ? JSON.parse(req.body.tags) : [], expiresAt: req.body.expiresAt ? new Date(req.body.expiresAt) : undefined } const result = await this.memoraiService.uploadFile(storageUpload, req.user!.id) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'File upload failed' }) } } private async handleStorageDownload(req: AuthRequest, res: Response): Promise<void> { try { const filePath = req.params.path // Extract file ID from path (assuming format: /user/year/month/filename) const pathParts = filePath.split('/') const fileId = pathParts[pathParts.length - 1].split('.')[0] const result = await this.memoraiService.downloadFile(fileId, req.user!.id) if (result.success && result.data) { // Set appropriate headers res.setHeader('Content-Type', 'application/octet-stream') res.setHeader('Content-Disposition', `attachment; filename="${filePath}"`) res.send(result.data) } else { res.status(404).json({ success: false, error: 'File not found' }) } } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'File download failed' }) } } private async handleStorageDelete(req: AuthRequest, res: Response): Promise<void> { try { const filePath = req.params.path const pathParts = filePath.split('/') const fileId = pathParts[pathParts.length - 1].split('.')[0] const result = await this.memoraiService.deleteFile(fileId, req.user!.id) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'File deletion failed' }) } } private async handleStorageList(req: AuthRequest, res: Response): Promise<void> { try { // TODO: Implement file listing by user res.json({ success: true, data: [], message: 'File listing not yet implemented' }) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'File listing failed' }) } } // Memory handlers private async handleMemoryStore(req: AuthRequest, res: Response): Promise<void> { try { const memory = { ...req.body, userId: req.user!.id } const result = await this.memoraiService.storeMemory(memory) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Memory storage failed' }) } } private async handleMemorySearch(req: AuthRequest, res: Response): Promise<void> { try { const query = { ...req.body, userId: req.user!.id } const result = await this.memoraiService.searchMemories(query) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Memory search failed' }) } } private async handleMemoryGet(req: AuthRequest, res: Response): Promise<void> { try { const { id } = req.params const result = await this.memoraiService.getMemory(id, req.user!.id) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Memory retrieval failed' }) } } private async handleMemoryUpdate(req: AuthRequest, res: Response): Promise<void> { try { const { id } = req.params const updates = req.body // TODO: Implement memory update res.json({ success: true, message: 'Memory update not yet implemented' }) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Memory update failed' }) } } private async handleMemoryDelete(req: AuthRequest, res: Response): Promise<void> { try { const { id } = req.params // TODO: Implement memory deletion res.json({ success: true, message: 'Memory deletion not yet implemented' }) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Memory deletion failed' }) } } // Analytics handlers private async handleAnalyticsTrack(req: AuthRequest, res: Response): Promise<void> { try { const event = { ...req.body, userId: req.user!.id, timestamp: new Date() } const result = await this.memoraiService.trackEvent(event) res.json(result) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Event tracking failed' }) } } private async handleAnalyticsQuery(req: AuthRequest, res: Response): Promise<void> { try { // TODO: Implement analytics querying res.json({ success: true, data: [], message: 'Analytics querying not yet implemented' }) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Analytics query failed' }) } } // Cache handlers private async handleCacheGet(req: AuthRequest, res: Response): Promise<void> { try { const { key } = req.params const value = await this.memoraiService.cacheGet(key) if (value !== null) { res.json({ success: true, data: value }) } else { res.status(404).json({ success: false, error: 'Cache key not found' }) } } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Cache get failed' }) } } private async handleCacheSet(req: AuthRequest, res: Response): Promise<void> { try { const { key } = req.params const { value, ttl, tags } = req.body await this.memoraiService.cacheSet(key, value, { ttl, tags }) res.json({ success: true }) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Cache set failed' }) } } private async handleCacheDelete(req: AuthRequest, res: Response): Promise<void> { try { const { key } = req.params const deleted = await this.memoraiService.cacheDelete(key) res.json({ success: true, data: deleted }) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Cache delete failed' }) } } // Sync handlers private async handleSyncStatus(req: AuthRequest, res: Response): Promise<void> { try { // TODO: Implement sync status res.json({ success: true, data: [], message: 'Sync status not yet implemented' }) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Sync status failed' }) } } private async handleSyncResolveConflict(req: AuthRequest, res: Response): Promise<void> { try { // TODO: Implement conflict resolution res.json({ success: true, message: 'Conflict resolution not yet implemented' }) } catch (error) { res.status(500).json({ success: false, error: error instanceof Error ? error.message : 'Conflict resolution failed' }) } } // ==================== ERROR HANDLING ==================== private setupErrorHandling(): void { // 404 handler this.app.use((req: Request, res: Response) => { res.status(404).json({ success: false, error: 'Endpoint not found', path: req.path }) }) // Global error handler this.app.use((error: Error, req: Request, res: Response, next: NextFunction) => { console.error('API Error:', error) res.status(500).json({ success: false, error: 'Internal server error', message: error.message }) }) } // ==================== GETTERS ==================== get isRunning(): boolean { return this._isRunning } get expressApp(): Express { return this.app } } // Export convenience function export function createMemoraiAPIServer(memoraiService: MemoraiService, config: MemoraiConfig): MemoraiAPIServer { return new MemoraiAPIServer(memoraiService, config) }