@flavoai/fastfold
Version:
Zero-boilerplate backend for React apps with auto-generated CRUD and declarative security
313 lines • 10.7 kB
JavaScript
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