@flavoai/fastfold
Version:
Zero-boilerplate backend for React apps with auto-generated CRUD and declarative security
706 lines âĸ 28.5 kB
JavaScript
// ============================================================================
// FASTFOLD - Zero-boilerplate backend for React apps
// ============================================================================
// Core types
export * from './types';
// Security API
export { Security, SecurityEnforcer } from './security';
// Database adapters
export { createDatabaseAdapter, DatabaseAdapters } from './database/adapters';
// Server
export { FastfoldServer, createFastfoldServer } from './server';
// CRUD generator (for advanced usage)
export { CrudGenerator } from './crud/generator';
// ============================================================================
// CONVENIENCE EXPORTS FOR COMMON USAGE
// ============================================================================
import express from 'express';
import path from 'path';
import fs from 'fs';
import cors from 'cors';
import helmet from 'helmet';
import jwt from 'jsonwebtoken';
import { Security } from './security';
import { DrizzleAdapter } from './database/adapters/drizzle';
/**
* Main Fastfold class for easy setup
*/
export class Fastfold {
static Security = Security;
/**
* Quick start implementation
*/
static async quickStart(config, port = 3001) {
// Detect if it's Drizzle-based or legacy format
if ('drizzle' in config) {
return await this.startWithDrizzle(config, port);
}
else {
return await this.startLegacy(config, port);
}
}
/**
* Start server with Drizzle integration
*/
static async startWithDrizzle(config, port) {
const { drizzle: drizzleConfig, tables, auth, uploads, rateLimit, endpoints, hooks, staticFrontend } = config;
// Create DrizzleAdapter
const adapter = new DrizzleAdapter(drizzleConfig.db, drizzleConfig.schema);
// Create Express server
const server = await this.createDrizzleServer({
adapter,
tables,
auth,
uploads,
rateLimit,
endpoints,
hooks,
port,
staticFrontend
});
// Execute server start hook
if (hooks?.onServerStart) {
await hooks.onServerStart(server);
}
await server.start(port);
return adapter;
}
/**
* Legacy start method for backward compatibility
*/
static async startLegacy(config, port) {
// Convert legacy config to new format for internal use
const legacyTables = {};
for (const [tableName, tableDef] of Object.entries(config.tables)) {
legacyTables[tableName] = {
security: tableDef.security,
operations: ['create', 'read', 'update', 'delete'] // All operations by default
};
}
// Create a minimal server for legacy support
// This would use the old database adapters
throw new Error('Legacy mode not yet implemented in new version. Please migrate to Drizzle.');
}
/**
* Create FastfoldServer with Drizzle adapter
*/
static async createDrizzleServer(options) {
const { adapter, tables, auth, uploads, rateLimit, endpoints, hooks, port } = options;
// Create Express app
const app = express();
// Setup middleware
app.use(cors());
app.use(helmet());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Setup authentication middleware if provided
if (auth) {
app.use((req, res, next) => {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.slice(7);
try {
const decoded = jwt.verify(token, auth.secret || 'your-secret-key');
req.user = decoded;
}
catch (err) {
// Invalid token - continue without user
}
}
next();
});
}
// Setup CRUD routes for each table
const tableNames = adapter.getTableNames().filter(name => tables[name]);
for (const tableName of tableNames) {
const tableConfig = tables[tableName];
const ops = tableConfig.operations || ['create', 'read', 'update', 'delete'];
// GET /api/tablename - List all
if (ops.includes('read')) {
app.get(`/api/${tableName}`, async (req, res) => {
try {
// Parse query parameters
const params = req.query.params ? JSON.parse(req.query.params) : req.query;
// Check if 'with' parameter exists to determine which query method to use
const hasRelations = params.with && Object.keys(params.with).length > 0;
const results = hasRelations
? await adapter.queryWithRelations(tableName, params)
: await adapter.query(tableName, params);
res.json({ success: true, data: results });
}
catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// GET /api/tablename/:id - Get by ID
app.get(`/api/${tableName}/:id`, async (req, res) => {
try {
// Parse query parameters for potential 'with' relations
const params = req.query.params ? JSON.parse(req.query.params) : req.query;
const queryParams = {
where: { id: parseInt(req.params.id) },
limit: 1,
...params
};
// Check if 'with' parameter exists
const hasRelations = queryParams.with && Object.keys(queryParams.with).length > 0;
const result = hasRelations
? await adapter.queryWithRelations(tableName, queryParams)
: await adapter.query(tableName, queryParams);
const item = result[0];
if (!item) {
return res.status(404).json({ success: false, error: 'Not found' });
}
res.json({ success: true, data: item });
}
catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
}
// POST /api/tablename - Create
if (ops.includes('create')) {
app.post(`/api/${tableName}`, async (req, res) => {
try {
const result = await adapter.create(tableName, req.body);
res.status(201).json({ success: true, data: result });
}
catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
}
// PUT /api/tablename/:id - Update
if (ops.includes('update')) {
app.put(`/api/${tableName}/:id`, async (req, res) => {
try {
const result = await adapter.update(tableName, parseInt(req.params.id), req.body);
res.json({ success: true, data: result });
}
catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
}
// DELETE /api/tablename/:id - Delete
if (ops.includes('delete')) {
app.delete(`/api/${tableName}/:id`, async (req, res) => {
try {
await adapter.delete(tableName, parseInt(req.params.id));
res.json({ success: true });
}
catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
}
}
// Add basic endpoints
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
// Add API docs endpoint
app.get('/docs', (req, res) => {
try {
const docsHtml = this.generateDrizzleDocs(adapter, tables);
res.setHeader('Content-Type', 'text/html');
res.send(docsHtml);
}
catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/docs/json', (req, res) => {
try {
const docsJson = this.generateDrizzleDocsJson(adapter, tables);
res.json(docsJson);
}
catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Setup custom endpoints
if (endpoints) {
endpoints(app);
}
// Serve static frontend (supports multiple mounts)
const staticCfg = options.staticFrontend;
const mounts = !staticCfg
? []
: Array.isArray(staticCfg)
? staticCfg
: [staticCfg];
for (const mount of mounts) {
const directory = mount.directory || path.resolve(process.cwd(), 'dist');
const urlPath = mount.urlPath || '/';
const indexFile = mount.indexFile || 'index.html';
const staticOptions = mount.staticOptions || { index: false };
if (!fs.existsSync(directory)) {
console.log(`âšī¸ Static mount skipped: directory not found -> ${directory}`);
continue;
}
// Mount static assets
if (urlPath === '/') {
app.use(express.static(directory, staticOptions));
}
else {
app.use(urlPath, express.static(directory, staticOptions));
}
// SPA fallback if requested
const spa = mount.spaFallback ?? (urlPath === '/');
if (spa) {
const defaultExcludes = ['/api', '/docs'];
const excludes = mount.excludePaths && mount.excludePaths.length > 0
? mount.excludePaths
: defaultExcludes;
const shouldExclude = (reqPath) => {
return excludes.some((p) => {
if (typeof p === 'string')
return reqPath.startsWith(p);
return p.test(reqPath);
});
};
const fallbackHandler = (req, res, next) => {
if (shouldExclude(req.path))
return next();
const filePath = path.join(directory, indexFile);
res.sendFile(filePath);
};
if (urlPath === '/') {
app.get('*', fallbackHandler);
}
else {
app.get(`${urlPath}*`, fallbackHandler);
}
console.log(`đī¸ Static mount: ${directory} -> ${urlPath} (SPA fallback enabled)`);
}
else {
console.log(`đī¸ Static mount: ${directory} -> ${urlPath}`);
}
}
// Create server object
const server = {
adapter,
tables,
app,
async start(serverPort) {
console.log(`đ Fastfold server starting on port ${serverPort}`);
console.log('đ Auto-generated CRUD endpoints:');
for (const tableName of tableNames) {
const tableConfig = tables[tableName];
const ops = tableConfig.operations || ['create', 'read', 'update', 'delete'];
console.log(` ${tableName}:`);
if (ops.includes('read')) {
console.log(` GET /api/${tableName} - List ${tableName}`);
console.log(` GET /api/${tableName}/:id - Get ${tableName} by ID`);
}
if (ops.includes('create')) {
console.log(` POST /api/${tableName} - Create ${tableName}`);
}
if (ops.includes('update')) {
console.log(` PUT /api/${tableName}/:id - Update ${tableName}`);
}
if (ops.includes('delete')) {
console.log(` DELETE /api/${tableName}/:id - Delete ${tableName}`);
}
}
if (endpoints) {
console.log('đ¯ Custom endpoints: Available via endpoints function');
}
// Actually start the HTTP server
app.listen(serverPort, () => {
console.log('â
Fastfold server ready!');
});
},
getApp() {
return this.app;
},
async shutdown() {
console.log('đ Shutting down Fastfold server...');
}
};
return server;
}
/**
* Generate HTML docs for Drizzle system
*/
static generateDrizzleDocs(adapter, tables) {
const { ApiExplorer } = require('./docs');
// Generate documentation data
const docs = this.generateDrizzleDocsJson(adapter, tables);
// Create mock doc generator to use existing ApiExplorer
const mockDocGenerator = {
generateDocumentation: () => docs
};
const explorer = new ApiExplorer(mockDocGenerator);
return explorer.generateHtml();
}
/**
* Generate JSON docs for Drizzle system
*/
static generateDrizzleDocsJson(adapter, tables) {
const tableNames = adapter.getTableNames().filter(name => tables[name]);
// Generate table documentation
const tablesDocs = {};
const endpoints = [];
for (const tableName of tableNames) {
const tableConfig = tables[tableName];
const ops = tableConfig.operations || ['create', 'read', 'update', 'delete'];
// Get schema from Drizzle table definition
let schema;
try {
const table = adapter.getTable(tableName);
schema = this.extractDrizzleSchema(table);
}
catch (error) {
// Fallback to mock schema if table not found
schema = this.getMockTableSchema(tableName);
}
// Add table documentation
tablesDocs[tableName] = {
schema,
security: this.getSecurityDescription(tableConfig.security)
};
// Generate endpoints for this table
endpoints.push(...this.generateDrizzleEndpoints(tableName, schema, ops));
}
// Add health endpoint
endpoints.unshift({
method: 'GET',
path: '/health',
description: 'Health check endpoint',
responses: [
{
status: 200,
description: 'Server is healthy',
schema: {
type: 'object',
properties: {
status: { type: 'string', example: 'healthy' },
timestamp: { type: 'string', example: '2024-01-01T00:00:00.000Z' }
}
}
}
]
});
return {
info: {
title: 'Fastfold API with Drizzle',
description: 'Auto-generated CRUD API with Drizzle ORM',
version: '1.0.0',
baseUrl: 'http://localhost:3001'
},
tables: tablesDocs,
endpoints
};
}
/**
* Extract schema from Drizzle table definition
*/
static extractDrizzleSchema(table) {
const schema = {};
// Handle mock database case - provide default schemas
if (!table || !table._ || !table._.columns) {
return this.getMockTableSchema(table);
}
// Real Drizzle table extraction
const columns = table._.columns || {};
for (const [columnName, column] of Object.entries(columns)) {
if (column && typeof column === 'object' && column.dataType) {
schema[columnName] = this.mapDrizzleType(column.dataType);
}
else {
// Fallback based on common column names
if (columnName === 'id')
schema[columnName] = 'number';
else if (columnName.includes('email'))
schema[columnName] = 'string';
else if (columnName.includes('name') || columnName.includes('title'))
schema[columnName] = 'string';
else if (columnName.includes('content') || columnName.includes('text'))
schema[columnName] = 'text';
else if (columnName.includes('published') || columnName.includes('active'))
schema[columnName] = 'boolean';
else if (columnName.includes('date') || columnName.includes('time'))
schema[columnName] = 'date';
else
schema[columnName] = 'string';
}
}
return schema;
}
/**
* Get mock table schema for demo purposes
*/
static getMockTableSchema(tableName) {
// Extract table name if it's an object with a name property
const name = typeof tableName === 'string' ? tableName :
tableName?.name || 'unknown';
// Return predefined schemas for known tables
const mockSchemas = {
users: {
id: 'number',
email: 'string',
name: 'string',
role: 'string',
createdAt: 'date'
},
posts: {
id: 'number',
title: 'string',
content: 'text',
published: 'boolean',
authorId: 'number',
createdAt: 'date'
},
comments: {
id: 'number',
text: 'text',
authorId: 'number',
postId: 'number',
createdAt: 'date'
}
};
return mockSchemas[name] || {
id: 'number',
name: 'string',
createdAt: 'date'
};
}
/**
* Map Drizzle data types to simple types
*/
static mapDrizzleType(drizzleType) {
const type = drizzleType.toLowerCase();
if (type.includes('text') || type.includes('varchar'))
return 'string';
if (type.includes('int') || type.includes('number'))
return 'number';
if (type.includes('bool'))
return 'boolean';
if (type.includes('date') || type.includes('time'))
return 'date';
if (type.includes('json'))
return 'json';
return 'string';
}
/**
* Generate endpoints documentation for a table
*/
static generateDrizzleEndpoints(tableName, schema, operations) {
const endpoints = [];
const basePath = `/api/${tableName}`;
// GET /api/:table - List records
if (operations.includes('read')) {
endpoints.push({
method: 'GET',
path: basePath,
description: `List all ${tableName} records`,
responses: [
{
status: 200,
description: 'List of records',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'array',
items: this.schemaToJsonSchema(schema)
}
}
}
}
]
});
// GET /api/:table/:id - Get single record
endpoints.push({
method: 'GET',
path: `${basePath}/{id}`,
description: `Get a specific ${tableName} record by ID`,
parameters: [
{
name: 'id',
in: 'path',
required: true,
type: 'number',
description: 'Record ID'
}
],
responses: [
{
status: 200,
description: 'Single record',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: this.schemaToJsonSchema(schema)
}
}
},
{ status: 404, description: 'Record not found' }
]
});
}
// POST /api/:table - Create record
if (operations.includes('create')) {
endpoints.push({
method: 'POST',
path: basePath,
description: `Create a new ${tableName} record`,
requestBody: {
description: `${tableName} data to create`,
schema: this.schemaToJsonSchema(schema)
},
responses: [
{
status: 201,
description: 'Record created successfully',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: this.schemaToJsonSchema(schema)
}
}
},
{ status: 400, description: 'Validation error' }
]
});
}
// PUT /api/:table/:id - Update record
if (operations.includes('update')) {
endpoints.push({
method: 'PUT',
path: `${basePath}/{id}`,
description: `Update a ${tableName} record`,
parameters: [
{
name: 'id',
in: 'path',
required: true,
type: 'number',
description: 'Record ID'
}
],
requestBody: {
description: `${tableName} data to update`,
schema: this.schemaToJsonSchema(schema)
},
responses: [
{
status: 200,
description: 'Record updated successfully',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: this.schemaToJsonSchema(schema)
}
}
},
{ status: 404, description: 'Record not found' },
{ status: 400, description: 'Validation error' }
]
});
}
// DELETE /api/:table/:id - Delete record
if (operations.includes('delete')) {
endpoints.push({
method: 'DELETE',
path: `${basePath}/{id}`,
description: `Delete a ${tableName} record`,
parameters: [
{
name: 'id',
in: 'path',
required: true,
type: 'number',
description: 'Record ID'
}
],
responses: [
{
status: 200,
description: 'Record deleted successfully',
schema: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
data: {
type: 'object',
properties: {
deleted: { type: 'boolean', example: true }
}
}
}
}
},
{ status: 404, description: 'Record not found' }
]
});
}
return endpoints;
}
/**
* Convert schema to JSON Schema format
*/
static schemaToJsonSchema(schema) {
const properties = {};
for (const [fieldName, fieldType] of Object.entries(schema)) {
switch (fieldType.toLowerCase()) {
case 'string':
properties[fieldName] = { type: 'string', example: `sample_${fieldName}` };
break;
case 'number':
case 'integer':
properties[fieldName] = { type: 'number', example: 42 };
break;
case 'boolean':
properties[fieldName] = { type: 'boolean', example: true };
break;
case 'date':
case 'datetime':
properties[fieldName] = { type: 'string', format: 'date-time', example: '2024-01-01T00:00:00.000Z' };
break;
case 'json':
properties[fieldName] = { type: 'object', example: { key: 'value' } };
break;
case 'text':
properties[fieldName] = { type: 'string', example: `Sample ${fieldName} text` };
break;
default:
properties[fieldName] = { type: 'string', example: `sample_${fieldName}` };
}
}
return {
type: 'object',
properties,
example: Object.fromEntries(Object.entries(properties).map(([key, value]) => [key, value.example]))
};
}
/**
* Get security description
*/
static getSecurityDescription(security) {
if (!security)
return 'No security configured';
if (typeof security === 'function')
return 'Custom security rule';
if (security.type === 'public')
return 'Public access - no authentication required';
if (security.type === 'admin')
return 'Admin only - requires admin role';
if (security.type === 'owner')
return 'Owner-based access - users can only access their own records';
if (security.type === 'team')
return 'Team-based access - users can access records from their team';
if (security.type === 'authenticated')
return 'Authenticated users only';
return 'Custom security rule';
}
}
// Default export
export default Fastfold;
//# sourceMappingURL=index.js.map