Reference implementation for the Phoenix Architecture. Work in progress.
aicoding.leaflet.pub/
ai
coding
crazy
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};