Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
at main 255 lines 9.5 kB view raw
1/** 2 * Runtime Target: node-typescript 3 * 4 * Compiles web-api architecture to Node.js + TypeScript. 5 * Stack: Hono (HTTP) + better-sqlite3 (DB) + Zod (validation) 6 */ 7 8import type { RuntimeTarget } from '../models/architecture.js'; 9 10// ─── Module template (LLM fills in marked sections) ───────────────────────── 11 12const MODULE_TEMPLATE = `import { Hono } from 'hono'; 13import { db, registerMigration } from '../../db.js'; 14import { z } from 'zod'; 15 16// ─── Database migrations ──────────────────────────────────────────────────── 17/* __MIGRATIONS__ */ 18 19// ─── Validation schemas ───────────────────────────────────────────────────── 20/* __SCHEMAS__ */ 21 22// ─── Routes ───────────────────────────────────────────────────────────────── 23const router = new Hono(); 24 25/* __ROUTES__ */ 26 27export default router; 28 29/* __PHOENIX_METADATA__ */ 30`; 31 32// ─── Shared files ─────────────────────────────────────────────────────────── 33 34const DB_FILE = `import Database from 'better-sqlite3'; 35import { existsSync, mkdirSync } from 'node:fs'; 36import { dirname } from 'node:path'; 37 38const DB_PATH = process.env.DB_PATH ?? 'data/app.db'; 39 40const dir = dirname(DB_PATH); 41if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); 42 43const db = new Database(DB_PATH); 44db.pragma('journal_mode = WAL'); 45db.pragma('foreign_keys = ON'); 46 47const migrations: Array<{ name: string; sql: string }> = []; 48 49export function registerMigration(name: string, sql: string): void { 50 migrations.push({ name, sql }); 51} 52 53export function runMigrations(): void { 54 for (const m of migrations) { 55 db.exec(m.sql); 56 } 57} 58 59export { db }; 60`; 61 62const APP_FILE = `import { Hono } from 'hono'; 63import { logger } from 'hono/logger'; 64import { cors } from 'hono/cors'; 65 66const app = new Hono(); 67 68app.use('*', logger()); 69app.use('*', cors()); 70 71app.get('/health', (c) => c.json({ status: 'ok', uptime: process.uptime() })); 72 73app.onError((err, c) => { 74 console.error('Unhandled error:', err.message, err.stack); 75 return c.json({ error: err.message }, 500); 76}); 77 78export function mount(path: string, router: Hono): void { 79 app.route(path, router); 80} 81 82export { app }; 83`; 84 85// ─── Prompt extension ─────────────────────────────────────────────────────── 86 87const PROMPT_EXTENSION = ` 88## Runtime: Node.js + TypeScript (Hono + better-sqlite3 + Zod) 89 90You are filling in sections of a module template. The imports, router, and exports are already provided. 91You MUST output ONLY the content for the marked sections, in this exact format: 92 93\`\`\` 94__MIGRATIONS__ 95registerMigration('tablename', \` 96 CREATE TABLE IF NOT EXISTS tablename ( 97 id INTEGER PRIMARY KEY AUTOINCREMENT, 98 ...columns... 99 created_at TEXT NOT NULL DEFAULT (datetime('now')) 100 ) 101\`); 102 103__SCHEMAS__ 104const CreateSchema = z.object({ ... }); 105const UpdateSchema = z.object({ ... }); 106 107__ROUTES__ 108router.get('/', (c) => { ... }); 109router.post('/', async (c) => { ... }); 110router.get('/:id', (c) => { ... }); 111router.patch('/:id', async (c) => { ... }); 112router.delete('/:id', (c) => { ... }); 113\`\`\` 114 115### Rules 116- Use better-sqlite3 synchronous API: db.prepare(sql).run(), .get(), .all() 117- Use parameterized queries ALWAYS — never interpolate user input into SQL 118- In SQL, use single quotes for string literals: datetime('now'). NEVER double quotes. 119- ALWAYS use snake_case for column names and JSON response keys 120- Nullable FK fields: z.number().int().nullable().optional() 121- FK validation: if (fk_id != null) { check exists } (loose equality) 122- LEFT JOIN to include related resource names (e.g., project_name) 123- Query parameter filtering: build WHERE clause dynamically from c.req.query() 124- Return created/updated resource after mutation 125- 200=read, 201=create, 204=delete, 400=validation, 404=not found 126 127### Web interface modules 128- Return c.html() with a complete HTML document 129- Use fetch('/resource-name') to call sibling API modules (no /api/ prefix) 130- Include ALL CSS and JavaScript inline 131`; 132 133// ─── Code examples ────────────────────────────────────────────────────────── 134 135const CODE_EXAMPLES = ` 136## Example: CRUD module sections for a "notes" resource 137 138\`\`\` 139__MIGRATIONS__ 140registerMigration('notes', \` 141 CREATE TABLE IF NOT EXISTS notes ( 142 id INTEGER PRIMARY KEY AUTOINCREMENT, 143 title TEXT NOT NULL, 144 body TEXT NOT NULL DEFAULT '', 145 category_id INTEGER REFERENCES categories(id), 146 created_at TEXT NOT NULL DEFAULT (datetime('now')) 147 ) 148\`); 149 150__SCHEMAS__ 151const CreateNoteSchema = z.object({ 152 title: z.string().min(1).max(200), 153 body: z.string().optional().default(''), 154 category_id: z.number().int().nullable().optional(), 155}); 156 157const UpdateNoteSchema = z.object({ 158 title: z.string().min(1).max(200).optional(), 159 body: z.string().optional(), 160 category_id: z.number().int().nullable().optional(), 161}); 162 163__ROUTES__ 164router.get('/', (c) => { 165 let sql = 'SELECT notes.*, categories.name as category_name FROM notes LEFT JOIN categories ON notes.category_id = categories.id'; 166 const conditions: string[] = []; 167 const params: unknown[] = []; 168 const categoryId = c.req.query('category_id'); 169 if (categoryId !== undefined) { conditions.push('notes.category_id = ?'); params.push(Number(categoryId)); } 170 if (conditions.length > 0) sql += ' WHERE ' + conditions.join(' AND '); 171 sql += ' ORDER BY notes.created_at DESC'; 172 return c.json(db.prepare(sql).all(...params)); 173}); 174 175router.get('/:id', (c) => { 176 const note = db.prepare('SELECT notes.*, categories.name as category_name FROM notes LEFT JOIN categories ON notes.category_id = categories.id WHERE notes.id = ?').get(c.req.param('id')); 177 if (!note) return c.json({ error: 'Not found' }, 404); 178 return c.json(note); 179}); 180 181router.post('/', async (c) => { 182 let body; try { body = await c.req.json(); } catch { return c.json({ error: 'Invalid JSON' }, 400); } 183 const result = CreateNoteSchema.safeParse(body); 184 if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 185 const { title, body: noteBody, category_id } = result.data; 186 if (category_id != null) { 187 if (!db.prepare('SELECT id FROM categories WHERE id = ?').get(category_id)) return c.json({ error: 'Category not found' }, 400); 188 } 189 const info = db.prepare('INSERT INTO notes (title, body, category_id) VALUES (?, ?, ?)').run(title, noteBody, category_id ?? null); 190 const note = db.prepare('SELECT notes.*, categories.name as category_name FROM notes LEFT JOIN categories ON notes.category_id = categories.id WHERE notes.id = ?').get(info.lastInsertRowid); 191 return c.json(note, 201); 192}); 193 194router.patch('/:id', async (c) => { 195 const id = c.req.param('id'); 196 if (!db.prepare('SELECT id FROM notes WHERE id = ?').get(id)) return c.json({ error: 'Not found' }, 404); 197 let body; try { body = await c.req.json(); } catch { return c.json({ error: 'Invalid JSON' }, 400); } 198 const result = UpdateNoteSchema.safeParse(body); 199 if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 200 const u = result.data; 201 if (u.title !== undefined) db.prepare('UPDATE notes SET title = ? WHERE id = ?').run(u.title, id); 202 if (u.body !== undefined) db.prepare('UPDATE notes SET body = ? WHERE id = ?').run(u.body, id); 203 if (u.category_id !== undefined) db.prepare('UPDATE notes SET category_id = ? WHERE id = ?').run(u.category_id, id); 204 return c.json(db.prepare('SELECT notes.*, categories.name as category_name FROM notes LEFT JOIN categories ON notes.category_id = categories.id WHERE notes.id = ?').get(id)); 205}); 206 207router.delete('/:id', (c) => { 208 const id = c.req.param('id'); 209 if (!db.prepare('SELECT id FROM notes WHERE id = ?').get(id)) return c.json({ error: 'Not found' }, 404); 210 db.prepare('DELETE FROM notes WHERE id = ?').run(id); 211 return c.body(null, 204); 212}); 213\`\`\` 214`; 215 216// ─── Export ───────────────────────────────────────────────────────────────── 217 218export const nodeTypescript: RuntimeTarget = { 219 name: 'node-typescript', 220 description: 'Node.js + TypeScript — Hono, better-sqlite3, Zod', 221 language: 'typescript', 222 223 packages: { 224 'hono': '^4.6.0', 225 '@hono/node-server': '^1.13.0', 226 'better-sqlite3': '^11.7.0', 227 'zod': '^3.24.0', 228 }, 229 230 devPackages: { 231 'typescript': '^5.4.0', 232 'vitest': '^2.0.0', 233 '@types/node': '^22.0.0', 234 '@types/better-sqlite3': '^7.6.0', 235 'tsx': '^4.0.0', 236 }, 237 238 moduleTemplate: MODULE_TEMPLATE, 239 promptExtension: PROMPT_EXTENSION, 240 codeExamples: CODE_EXAMPLES, 241 242 sharedFiles: { 243 'src/db.ts': DB_FILE, 244 'src/app.ts': APP_FILE, 245 }, 246 247 packageExtras: { 248 scripts: { 249 dev: 'tsx watch src/server.ts', 250 start: 'tsx src/server.ts', 251 build: 'tsc', 252 test: 'vitest run', 253 }, 254 }, 255};