UNPKG

@flavoai/fastfold

Version:

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

937 lines (771 loc) 25.8 kB
# Fastfold Zero-boilerplate backend for React apps with **Drizzle ORM** integration, auto-generated CRUD, and declarative security. **React Query powered** for smart caching and background sync. Perfect for rapid UI development with minimal code. ## Features - 🚀 **Zero boilerplate** - Auto-generated CRUD operations from Drizzle schemas - 🎯 **Simple API** - Just `useQuery` and `useMutation` (powered by React Query!) - 📦 **Automatic caching** - Smart cache invalidation and background refetching - 🗄️ **Drizzle ORM** - Full database power with relationships, migrations, and validation - 🔒 **Declarative security** - Role-based permissions per table - 🔧 **TypeScript** - Full type safety out of the box - 🤖 **LLM-friendly** - Small, predictable API surface - ⚡ **Production ready** - PostgreSQL, MySQL, SQLite support with foreign keys - 🛠️ **React Query DevTools** - Optional debugging and cache inspection ## Quick Start ### 1. Install Fastfold ```bash npm install @flavoai/fastfold @tanstack/react-query ``` **Note**: React Query is a peer dependency for the client hooks. ### 2. Define Your Schema with Drizzle (30 seconds) ```typescript // schema.ts - Use Drizzle's powerful schema definition import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'; import { relations } from 'drizzle-orm'; export const users = sqliteTable('users', { id: integer('id').primaryKey({ autoIncrement: true }), email: text('email').notNull().unique(), name: text('name').notNull(), role: text('role').default('user'), }); export const posts = sqliteTable('posts', { id: integer('id').primaryKey({ autoIncrement: true }), title: text('title').notNull(), content: text('content'), published: integer('published', { mode: 'boolean' }).default(false), authorId: integer('author_id').references(() => users.id, { onDelete: 'cascade' }), }); // Define relationships export const usersRelations = relations(users, ({ many }) => ({ posts: many(posts), })); export const postsRelations = relations(posts, ({ one }) => ({ author: one(users, { fields: [posts.authorId], references: [users.id] }), })); ``` ### 3. Create Your Backend (30 seconds) ```typescript // server.ts - Fastfold adds CRUD + Security to your Drizzle schema import Fastfold, { Security } from '@flavoai/fastfold'; import path from 'path'; import { drizzle } from 'drizzle-orm/better-sqlite3'; import Database from 'better-sqlite3'; import * as schema from './schema'; const db = drizzle(new Database('./app.db'), { schema }); await Fastfold.quickStart({ // 🔗 DRIZZLE INTEGRATION - Use your existing schema drizzle: { db, schema }, // 🔒 ADD SECURITY - Just specify security per table tables: { users: { security: Security.authenticated(), operations: ['read', 'update'] // Granular CRUD control }, posts: { security: Security.owner('authorId'), operations: ['create', 'read', 'update', 'delete'] } }, // 🧩 (Optional) Serve your frontend SPA from the same server // Supports single or multiple static mounts staticFrontend: [ { directory: path.resolve(process.cwd(), 'dist'), // your Vite build output urlPath: '/', // mount at root spaFallback: true, // route non-API requests to index.html excludePaths: ['/api', '/docs', /^\/assets\//], indexFile: 'index.html', staticOptions: { index: false, maxAge: '1h', immutable: true } }, // Example: admin panel mounted at /admin // { // directory: path.resolve(process.cwd(), 'admin-dist'), // urlPath: '/admin', // spaFallback: true, // excludePaths: ['/api', '/docs'] // } ] }); // That's it! Full CRUD API with relationships and security ``` ### 4. Build and Serve your Frontend (optional) If you want to serve your SPA from Fastfold, build it with Vite (or similar) into `dist/` (or your configured directory): ```bash # From your frontend project npm run build # Ensure build outputs to ./dist relative to your server or set staticFrontend.directory accordingly ``` Then start Fastfold; your app will be served along with the API. ### 5. Minimal frontend you can copy-paste (CSP-friendly) Create `dist/index.html` with: ```html <!doctype html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Fastfold Quickstart</title> <link rel="stylesheet" href="app.css" /> </head> <body> <h1>Fastfold Quickstart</h1> <p>Backend API is mounted at <code>/api</code>. Click to seed sample data, then list posts.</p> <div> <button id="seed">Seed Sample Data</button> <button id="load">Load Posts</button> </div> <div id="out" style="margin-top: 1rem"></div> <script defer src="app.js"></script> </body> </html> ``` Create `dist/app.js` with: ```js const out = document.getElementById('out'); const show = (html) => { out.innerHTML = html; }; document.getElementById('seed').onclick = async () => { const res = await fetch('/api/seed', { method: 'POST', headers: { 'content-type': 'application/json' } }); const data = await res.json(); show(`<div class="card">Seeded: <pre>${JSON.stringify(data, null, 2)}</pre></div>`); }; document.getElementById('load').onclick = async () => { const res = await fetch('/api/posts?params=' + encodeURIComponent(JSON.stringify({ with: { author: true }, orderBy: { createdAt: 'desc' } }))); const json = await res.json(); const posts = json.data || []; show(posts.length ? posts.map(p => `<div class="card"><h3>${p.title}</h3><div>By: ${p.author?.name || 'Unknown'}</div><p>${p.content}</p></div>`).join('') : '<div class="card">No posts yet</div>'); }; ``` Create `dist/app.css` with: ```css body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji'; margin: 2rem; } h1 { margin: 0 0 1rem; } button { padding: .5rem .75rem; } .card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: .5rem 0; } code { background: #f6f8fa; padding: .2rem .4rem; border-radius: 4px; } ``` Now start the server and visit `http://localhost:3001`. ### 6. Use in React (30 seconds) ```tsx // App.tsx - Wrap your app with FastfoldProvider import { FastfoldProvider, configureFastfold } from '@flavoai/fastfold/client'; // Configure your API endpoint configureFastfold({ baseUrl: 'http://localhost:3001/api' }); function App() { return ( <FastfoldProvider> <BlogList /> </FastfoldProvider> ); } // BlogList.tsx - Use Fastfold hooks (powered by React Query!) import { useQuery, useCreate } from '@flavoai/fastfold/client'; function BlogList() { // 🎯 SIMPLE QUERY - Fetch data with relationships (with React Query caching!) const { data: posts, isLoading, error } = useQuery('posts', { where: { published: true }, orderBy: { createdAt: 'desc' }, with: { author: true } // Include relationships! }); // 🚀 SIMPLE MUTATION - Create data with automatic cache invalidation const createPost = useCreate('posts'); const handleCreate = () => { createPost.mutate({ title: 'Hello World', content: 'My first post!', published: true }); // Cache automatically updates! No manual refetch needed 🎉 }; if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> <button onClick={handleCreate} disabled={createPost.isPending} > {createPost.isPending ? 'Creating...' : 'Create Post'} </button> {posts?.map(post => ( <div key={post.id}> <h3>{post.title}</h3> <p>By: {post.author?.name}</p> <p>{post.content}</p> </div> ))} </div> ); } ``` ## 🔒 Security Made Simple Fastfold comes with built-in, declarative security that's both powerful and easy to use: ```typescript // 🔓 LEVEL 1: ZERO-SECURITY (0 lines of security code) // Perfect for: public data, prototypes blog: { schema: { title: 'string', content: 'string' }, security: Security.public() // Everyone can access ✨ }, // 🔐 LEVEL 2: ONE-LINER SECURITY (1 line of security code) // Perfect for: most common use cases adminLogs: { schema: { action: 'string', timestamp: 'number' }, security: Security.admin() // Only admins ✨ }, userProfiles: { schema: { userId: 'string', name: 'string', bio: 'string' }, security: Security.owner('userId') // Users own their data ✨ }, // ⚙️ LEVEL 3: CUSTOM SECURITY (as complex as you need) // Perfect for: complex business rules projects: { schema: { name: 'string', teamId: 'string' }, security: Security.custom((ctx) => { return ctx.user?.teamId === ctx.data?.teamId; }) } ``` ## API Reference ### Server API ```typescript import Fastfold, { Security } from '@flavoai/fastfold'; // Quick start with Drizzle (recommended) const adapter = await Fastfold.quickStart({ drizzle: { db, schema }, tables: { users: { security: Security.authenticated(), operations: ['read', 'update'] }, posts: { security: Security.owner('authorId'), operations: ['create', 'read', 'update', 'delete'] } } }); // Advanced configuration with custom endpoints const adapter = await Fastfold.quickStart({ drizzle: { db, schema }, tables: { /* your table configs */ }, auth: { secret: 'your-jwt-secret', expiresIn: '24h' }, endpoints: (app) => { // Add custom endpoints app.get('/api/custom', (req, res) => { res.json({ message: 'Custom endpoint' }); }); }, hooks: { onServerStart: async (server) => { console.log('Server started on port', server.port); } } }); ``` ### Client API ```typescript import { useQuery, // Fetch multiple records useQueryOne, // Fetch single record useCreate, // Create records useUpdate, // Update records useDelete, // Delete records configureFastfold, setAuthToken } from '@flavoai/fastfold/client'; // Configure client configureFastfold({ baseUrl: 'http://localhost:3001/api' }); // Set authentication setAuthToken('your-jwt-token'); ``` #### ⚙️ **Client Configuration** ```tsx import { configureFastfold, setAuthToken } from '@flavoai/fastfold/client'; // Basic configuration configureFastfold({ baseUrl: 'https://api.myapp.com/api', // Your API base URL headers: { 'X-App-Version': '1.0.0', 'X-Custom-Header': 'value' } }); // Authentication setup function LoginComponent() { const handleLogin = async (email, password) => { // Your login logic here const response = await fetch('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }); const { token } = await response.json(); // Set auth token for all future requests setAuthToken(token); // Now all useQuery, useCreate, etc. will include this token }; const handleLogout = () => { // Clear the auth token setAuthToken(''); // or configureFastfold({ headers: {} // Clear all headers }); }; return ( <div> <button onClick={handleLogin}>Login</button> <button onClick={handleLogout}>Logout</button> </div> ); } ``` ### Complete Client Examples #### 📋 **Querying Data** ```tsx import { useQuery, useQueryOne } from '@flavoai/fastfold/client'; function PostsList() { // Fetch multiple posts with relationships const { data: posts, isLoading, error } = useQuery('posts', { where: { published: true }, orderBy: { createdAt: 'desc' }, with: { author: true }, // Include author data limit: 10 }); // Fetch single post by ID const { data: post } = useQueryOne('posts', '123', { with: { author: true, comments: true } }); if (isLoading) return <div>Loading posts...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> {posts?.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>By: {post.author?.name}</p> <p>{post.content}</p> </article> ))} </div> ); } ``` #### ✏️ **Creating Data** ```tsx import { useCreate } from '@flavoai/fastfold/client'; function CreatePostForm() { const createPost = useCreate('posts', { onSuccess: (newPost) => { console.log('Post created:', newPost); // Cache automatically updated - no refetch needed! }, onError: (error) => { console.error('Failed to create post:', error); } }); const handleSubmit = async (formData) => { try { const newPost = await createPost.mutate({ title: formData.title, content: formData.content, published: true, authorId: currentUser.id }); console.log('Created post:', newPost); } catch (error) { console.error('Creation failed:', error); } }; return ( <form onSubmit={handleSubmit}> {/* form fields */} <button type="submit" disabled={createPost.isLoading} > {createPost.isLoading ? 'Creating...' : 'Create Post'} </button> </form> ); } ``` #### 🔄 **Updating Data** ```tsx import { useUpdate } from '@flavoai/fastfold/client'; function EditPostForm({ post }) { const updatePost = useUpdate('posts', { onSuccess: (updatedPost) => { console.log('Post updated:', updatedPost); // Navigate back or show success message }, onError: (error) => { console.error('Update failed:', error); } }); const handleUpdate = async (formData) => { try { const updatedPost = await updatePost.mutate({ id: post.id, data: { title: formData.title, content: formData.content, published: formData.published } }); console.log('Updated post:', updatedPost); } catch (error) { console.error('Update failed:', error); } }; return ( <form onSubmit={handleUpdate}> {/* form fields */} <button type="submit" disabled={updatePost.isLoading} > {updatePost.isLoading ? 'Updating...' : 'Update Post'} </button> {updatePost.error && ( <div className="error"> Error: {updatePost.error.message} </div> )} </form> ); } ``` #### 🗑️ **Deleting Data** ```tsx import { useDelete } from '@flavoai/fastfold/client'; function PostItem({ post, onDeleted }) { const deletePost = useDelete('posts', { onSuccess: () => { console.log('Post deleted successfully'); onDeleted?.(post.id); // Notify parent component }, onError: (error) => { console.error('Delete failed:', error); } }); const handleDelete = async () => { if (confirm('Are you sure you want to delete this post?')) { try { await deletePost.mutate(post.id); } catch (error) { console.error('Delete failed:', error); } } }; return ( <div className="post-item"> <h3>{post.title}</h3> <p>{post.content}</p> <button onClick={handleDelete} disabled={deletePost.isLoading} className="delete-btn" > {deletePost.isLoading ? 'Deleting...' : 'Delete'} </button> </div> ); } ``` #### 🔄 **Complete CRUD Example** ```tsx import { useQuery, useCreate, useUpdate, useDelete } from '@flavoai/fastfold/client'; function PostsManager() { // Query posts - React Query handles caching automatically const { data: posts, isLoading } = useQuery('posts', { with: { author: true }, orderBy: { createdAt: 'desc' } }); // CRUD operations - cache automatically invalidated! const createPost = useCreate('posts'); const updatePost = useUpdate('posts'); const deletePost = useDelete('posts'); const handleCreate = (data) => createPost.mutate(data); const handleUpdate = (id, data) => updatePost.mutate({ id, data }); const handleDelete = (id) => deletePost.mutate(id); if (isLoading) return <div>Loading...</div>; return ( <div> <CreateForm onSubmit={handleCreate} /> {posts?.map(post => ( <PostCard key={post.id} post={post} onUpdate={handleUpdate} onDelete={handleDelete} /> ))} </div> ); } ``` #### 🔄 **Cache Management with React Query** Fastfold uses React Query for automatic cache management! Here's how it works: ##### **Automatic Cache Invalidation (Default Behavior)** ```tsx function PostsList() { // React Query handles all caching automatically! const { data: posts, isLoading, error } = useQuery('posts', { with: { author: true } }); const createPost = useCreate('posts'); const updatePost = useUpdate('posts'); const deletePost = useDelete('posts'); const handleCreate = () => { createPost.mutate({ title: 'New Post', content: 'Hello World!' }); // ✨ Cache automatically invalidated and refetched! // No manual refetch needed! }; // React Query provides: // - 5min stale time with background refetch // - Automatic cache invalidation after mutations // - Smart deduplication of identical requests // - Error retry with exponential backoff } ``` ##### **Advanced: Custom Cache Options** ```tsx function PostsList() { // Customize React Query behavior const { data: posts } = useQuery('posts', { with: { author: true } }, { staleTime: 10 * 60 * 1000, // 10 minutes gcTime: 30 * 60 * 1000, // 30 minutes refetchOnWindowFocus: false, refetchInterval: 2 * 60 * 1000 // Poll every 2 minutes }); const createPost = useCreate('posts', { onSuccess: (newPost) => { console.log('✅ Post created:', newPost); // Cache automatically updates - no manual work needed! }, onError: (error) => { console.error('❌ Failed to create post:', error); // React Query handles retries automatically } }); // React Query DevTools (optional) // Add <ReactQueryDevtools /> to see cache state } ``` ##### **Advanced: Cache Utilities for Power Users** ```tsx import { useInvalidateCache, useUpdateCache } from '@flavoai/fastfold/client'; function PostEditor() { const invalidateCache = useInvalidateCache(); const updateCache = useUpdateCache(); const updatePost = useUpdate('posts', { onSuccess: (updatedPost) => { // Option 1: Invalidate specific cache keys invalidateCache(['posts']); // Refetch all posts queries // Option 2: Update cache directly (optimistic) updateCache(['posts'], (oldData) => oldData?.map(post => post.id === updatedPost.id ? updatedPost : post ) ); } }); const handleBulkUpdate = async () => { // For complex operations, invalidate multiple caches await bulkUpdatePosts(); invalidateCache(['posts', 'users', 'stats']); }; } ``` > **✨ React Query Power**: Fastfold uses React Query under the hood, giving you all its benefits: > - **Smart caching** with configurable stale times > - **Background refetching** to keep data fresh > - **Automatic deduplication** of identical requests > - **Error retry** with exponential backoff > - **Optimistic updates** and rollback on error > - **DevTools integration** for debugging cache state > > Most apps won't need manual cache management - React Query handles it all automatically! 🎉 ### Security API ```typescript import { Security } from '@flavoai/fastfold/client'; Security.public() // Anyone can access Security.admin() // Only admin role Security.owner('userId') // Owner-based access Security.team('teamId') // Team-based access Security.authenticated() // Any logged-in user Security.readOnlyPublic() // Public read, admin write Security.custom((ctx) => boolean) // Custom logic ``` ## Auto-Generated API Endpoints Fastfold automatically creates CRUD endpoints based on your `operations` config: ### Users (operations: ['read', 'update']) ``` GET /api/users ✅ List users GET /api/users/:id ✅ Get user PUT /api/users/:id ✅ Update user POST /api/users ❌ Blocked DELETE /api/users/:id ❌ Blocked ``` ### Posts (operations: ['create', 'read', 'update', 'delete']) ``` GET /api/posts ✅ List posts (with security filtering) GET /api/posts/:id ✅ Get post POST /api/posts ✅ Create post PUT /api/posts/:id ✅ Update post (owner only) DELETE /api/posts/:id ✅ Delete post (owner only) // Relationship endpoints (automatic from Drizzle relations) GET /api/posts/:id/author ✅ Get post's author GET /api/posts/:id/comments ✅ Get post's comments GET /api/users/:id/posts ✅ Get user's posts ``` ### Query Parameters ``` GET /api/posts?where={"published":true}&orderBy={"createdAt":"desc"}&limit=10 GET /api/posts?with={"author":true,"comments":true} // Include relations ``` ## Examples See the `examples/` directory for complete examples: - [Quickstart Backend](examples/quickstart.ts) - [React Client Usage](examples/react-client.tsx) ## Development ```bash # Install dependencies npm install # Start development server npm run dev # Build for production npm run build # Run tests npm test ``` ## Database Operations Fastfold automatically handles database setup with zero configuration: ```bash # Auto-migration: Tables are created automatically when server starts npm run dev # Creates tables from your schema definitions # Reset database: Simply delete the database file rm ./fastfold.db # Default SQLite database file npm run dev # Recreates tables on next start # Custom database location const adapter = await Fastfold.quickStart({ drizzle: { db: drizzle(new Database('./my-app.db'), { schema }), schema }, tables: { /* your table configs */ } }); ``` **Note**: Fastfold uses auto-migration - tables are created/updated automatically based on your schema definitions. No manual migration commands needed. ## Advanced Features ### 🔒 Granular CRUD Control Control exactly which operations are allowed per table: ```typescript await Fastfold.quickStart({ drizzle: { db, schema }, tables: { users: { security: Security.authenticated(), operations: ['read', 'update'] // Only GET and PUT endpoints }, posts: { security: Security.owner('authorId'), operations: ['create', 'read', 'update', 'delete'] // Full CRUD }, comments: { security: Security.custom((ctx) => ctx.user?.id === ctx.data?.authorId), operations: ['create', 'read', 'delete'] // No updates allowed } } }); ``` ### 🎯 Custom Endpoints with Full Drizzle Power ```typescript const adapter = await Fastfold.quickStart({ drizzle: { db, schema }, tables: { /* your table configs */ }, endpoints: (app) => { // Use Drizzle's relational queries app.get('/api/trending', Security.public(), async (req, res) => { const trending = await adapter.queryWithRelations('posts', { with: { author: { columns: { name: true } }, comments: true }, where: { published: true }, orderBy: { createdAt: 'desc' }, limit: 10 }); res.json(trending); }); // Complex business logic app.post('/api/newsletter', Security.admin(), async (req, res) => { const users = await adapter.query('users', { where: { role: 'subscriber' } }); // Send emails... res.json({ sent: users.length }); }); } }); // Use adapter anywhere in your app export { adapter }; ``` ### 🚀 Advanced Configuration ```typescript await Fastfold.quickStart({ drizzle: { db, schema }, tables: { /* your table configs */ }, // Authentication configuration auth: { secret: 'your-jwt-secret', expiresIn: '24h' }, // Custom endpoints endpoints: (app) => { app.get('/api/trending', async (req, res) => { const trending = await adapter.queryWithRelations('posts', { with: { author: true }, where: { published: true }, orderBy: { createdAt: 'desc' }, limit: 10 }); res.json(trending); }); }, // Server hooks hooks: { onServerStart: async (server) => { console.log('🚀 Server ready on port', server.port); } } }); }); ``` ## Why Fastfold? **Perfect for LLMs and rapid development:** 1. **Drizzle ORM Power** - Full database relationships, migrations, validation 2. **Tiny API surface** - Just define Drizzle schema + security rules 3. **Zero boilerplate** - Auto-generated CRUD with granular control 4. **Type-safe by default** - Full TypeScript from database to frontend 5. **Production ready** - PostgreSQL, MySQL, SQLite with foreign keys 6. **Extensible** - Full Express access + Drizzle queries when needed **Perfect for:** ✅ **Production applications** - Full database power with Drizzle ✅ **Rapid prototyping** - Zero-config CRUD generation ✅ **LLM-assisted development** - Minimal, predictable API ✅ **React + Backend** - Unified TypeScript experience ✅ **Complex data models** - Relationships, constraints, validation ✅ **Custom business logic** - Express endpoints + Drizzle queries **Not suitable for:** ❌ **Microservices architecture** - Single server design ❌ **Non-database apps** - Focused on CRUD operations ❌ **GraphQL requirements** - REST API only ❌ **Real-time by default** - WebSockets require custom endpoints ## License MIT