UNPKG

@flavoai/fastfold

Version:

Zero-boilerplate backend for React apps with auto-generated CRUD and declarative security

313 lines 10.7 kB
import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import jwt from 'jsonwebtoken'; import { createDatabaseAdapter } from '../database/adapters'; import { CrudGenerator } from '../crud/generator'; import { ApiDocumentationGenerator, ApiExplorer } from '../docs'; export class FastfoldServer { app; config; db; crudGenerator; docGenerator; apiExplorer; constructor(config) { this.config = config; this.app = express(); this.setupMiddleware(); this.db = createDatabaseAdapter(config.database.provider, config.database.connection); this.crudGenerator = new CrudGenerator(this.db, config.tables); this.docGenerator = new ApiDocumentationGenerator(config); this.apiExplorer = new ApiExplorer(this.docGenerator); } /** * Initialize the server - connect to database and create tables */ async initialize() { await this.db.connect(); // Create all tables based on schema for (const [tableName, tableDefinition] of Object.entries(this.config.tables)) { await this.db.createTable(tableName, tableDefinition.schema); } this.setupRoutes(); } /** * Start the server */ async start(port) { await this.initialize(); const serverPort = port || this.config.port || 3001; this.app.listen(serverPort, () => { console.log(`🚀 Fastfold server running on http://localhost:${serverPort}`); console.log(`📊 Database: ${this.config.database.provider}`); console.log(`🔧 Tables: ${Object.keys(this.config.tables).join(', ')}`); }); } /** * Get the Express app instance */ getApp() { return this.app; } /** * Setup middleware */ setupMiddleware() { // Security with CSP exception for docs page this.app.use((req, res, next) => { if (req.path === '/docs') { // Allow inline scripts for docs page helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], scriptSrcAttr: ["'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], }, }, })(req, res, next); } else { // Use default helmet for other routes helmet()(req, res, next); } }); this.app.use(cors()); // Body parsing this.app.use(express.json({ limit: '10mb' })); this.app.use(express.urlencoded({ extended: true })); // Request logging this.app.use((req, res, next) => { console.log(`${req.method} ${req.path}`); next(); }); // Auth middleware this.app.use(this.authMiddleware.bind(this)); } /** * Authentication middleware */ authMiddleware(req, res, next) { const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ') && this.config.auth?.secret) { const token = authHeader.substring(7); try { const decoded = jwt.verify(token, this.config.auth.secret); req.user = decoded; } catch (error) { console.warn('Invalid JWT token:', error); } } next(); } /** * Setup API routes for all tables */ setupRoutes() { // Health check this.app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString() }); }); // API Documentation endpoints this.app.get('/docs', (req, res) => { res.setHeader('Content-Type', 'text/html'); res.send(this.apiExplorer.generateHtml()); }); this.app.get('/docs/json', (req, res) => { res.json(this.docGenerator.generateDocumentation()); }); // Generate routes for each table for (const tableName of Object.keys(this.config.tables)) { this.setupTableRoutes(tableName); } // 404 handler this.app.use((req, res) => { res.status(404).json({ success: false, error: `Route not found: ${req.method} ${req.path}` }); }); // Error handler this.app.use((error, req, res, next) => { console.error('Server error:', error); res.status(500).json({ success: false, error: error.message || 'Internal server error' }); }); } /** * Setup CRUD routes for a specific table */ setupTableRoutes(tableName) { const operations = this.crudGenerator.generateTableOperations(tableName); const basePath = `/api/${tableName}`; // GET /api/:table - Find many records this.app.get(basePath, async (req, res) => { try { const params = req.query.params ? JSON.parse(req.query.params) : {}; const context = this.createRequestContext(req, res); const data = await operations.findMany(params, context); const count = await operations.count(params.where, context); res.json({ success: true, data, count }); } catch (error) { this.handleError(error, res); } }); // GET /api/:table/:id - Find one record this.app.get(`${basePath}/:id`, async (req, res) => { try { const id = this.parseId(req.params.id); const context = this.createRequestContext(req, res); const data = await operations.findOne(id, context); res.json({ success: true, data }); } catch (error) { this.handleError(error, res); } }); // POST /api/:table - Create record this.app.post(basePath, async (req, res) => { try { const context = this.createRequestContext(req, res); const data = await operations.create(req.body, context); res.status(201).json({ success: true, data }); } catch (error) { this.handleError(error, res); } }); // PUT /api/:table/:id - Update record this.app.put(`${basePath}/:id`, async (req, res) => { try { const id = this.parseId(req.params.id); const context = this.createRequestContext(req, res); const data = await operations.update(id, req.body, context); res.json({ success: true, data }); } catch (error) { this.handleError(error, res); } }); // DELETE /api/:table/:id - Delete record this.app.delete(`${basePath}/:id`, async (req, res) => { try { const id = this.parseId(req.params.id); const context = this.createRequestContext(req, res); const success = await operations.delete(id, context); res.json({ success, data: { deleted: success } }); } catch (error) { this.handleError(error, res); } }); // GET /api/:table/count - Count records this.app.get(`${basePath}/count`, async (req, res) => { try { const where = req.query.where ? JSON.parse(req.query.where) : undefined; const context = this.createRequestContext(req, res); const count = await operations.count(where, context); res.json({ success: true, data: { count } }); } catch (error) { this.handleError(error, res); } }); } /** * Create request context for security checks */ createRequestContext(req, res) { return { req, res, user: req.user, operation: 'read', // Will be overridden by specific operations tableName: '' // Will be set by specific operations }; } /** * Parse ID parameter (handles both string and number IDs) */ parseId(id) { const numId = parseInt(id, 10); return isNaN(numId) ? id : numId; } /** * Handle errors consistently */ handleError(error, res) { console.error('API Error:', error); // Check if it's an authorization error if (error.message.includes('Unauthorized')) { res.status(403).json({ success: false, error: error.message }); return; } // Check if it's a not found error if (error.message.includes('not found')) { res.status(404).json({ success: false, error: error.message }); return; } // Check if it's a validation error if (error.message.includes('Missing required field') || error.message.includes('Invalid type for field')) { res.status(400).json({ success: false, error: error.message }); return; } // Default to 500 for other errors res.status(500).json({ success: false, error: error.message }); } /** * Graceful shutdown */ async shutdown() { console.log('Shutting down Fastfold server...'); await this.db.disconnect(); console.log('Database disconnected.'); } } // ============================================================================ // CONVENIENCE FUNCTION // ============================================================================ /** * Quick start function for Fastfold */ export function createFastfoldServer(config) { return new FastfoldServer(config); } //# sourceMappingURL=index.js.map