Figuring this shit out
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add SQLite database layer and guestbook API routes

aria f0ed7737 b8e5cab3

+152 -8
+86
src/db.ts
··· 1 + import Database from "better-sqlite3"; 2 + import {resolve, dirname} from "node:path"; 3 + import { fileURLToPath } from "node:url"; 4 + 5 + const __dirname = dirname(fileURLToPath(import.meta.url)); 6 + 7 + export interface Entry { 8 + id: number; 9 + user_did: string; 10 + user_handle: string; 11 + message: string; 12 + created_at: string; 13 + } 14 + 15 + export interface NewEntry { 16 + user_did: string; 17 + user_handle: string; 18 + message: string; 19 + } 20 + 21 + 22 + // TODO: allow customization of DB_PATH 23 + // Set db location 24 + const DB_PATH = resolve(__dirname, "..", "guestbook.db"); 25 + 26 + const db = new Database(DB_PATH); 27 + 28 + db.pragma("journal_mode = WAL"); 29 + 30 + // Setup new db 31 + db.exec(` 32 + CREATE TABLE IF NOTE EXISTS entries ( 33 + id INTEGER PRIMARY KEY AUTOINCREMENT, 34 + user_id TEXT NOT NULL 35 + user_handle TEXT NOT NULL, 36 + message TEXT NOT NULL, 37 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 38 + ) 39 + `); 40 + 41 + console.log("Database ready at", DB_PATH); 42 + 43 + // === 44 + // SQL functions 45 + // === 46 + 47 + const stmtGetAll = db.prepare(` 48 + SELECT id, user_did, user_handle, message, created_at 49 + FROM entries 50 + ORDER BY created_at DESC 51 + `); 52 + 53 + const stmtInsert = db.prepare(` 54 + INSERT INTO entries (user_did, user_handle, message) 55 + VALUES (@user_did, @user_handle, @message) 56 + `); 57 + 58 + const stmtDelete = db.prepare(` 59 + DELETE FROM entries WHERE id = @id AND user_did = @user_did 60 + `); 61 + 62 + export function getAllEntries(): Entry[] { 63 + // IT CAN JUST DO THAT?? `as` is cool! 64 + return stmtGetAll.all() as Entry[]; 65 + } 66 + 67 + export function createEntry(entry: NewEntry): Entry { 68 + const result = stmtInsert.run(entry); 69 + 70 + return { 71 + id: Number(result.lastInsertRowid), 72 + created_at: new Date().toISOString(), 73 + ...entry, 74 + } 75 + } 76 + 77 + /** 78 + * deletes an entry in the DB if the id and userDID are both accurate. 79 + * @param {number} id - The ID of the post. 80 + * @param {string} userDid - The DID provided by the user. 81 + * @returns {boolean} where changes made to the DB? 82 + */ 83 + export function deleteEntry(id: number, userDid: string): boolean { 84 + const result = stmtDelete.run({ id, user_did: userDid}); 85 + return result.changes > 0; 86 + }
+66 -8
src/index.ts
··· 3 3 import { readFileSync } from "node:fs"; 4 4 import { resolve, dirname } from "node:path"; 5 5 import { fileURLToPath } from "node:url"; 6 + import { getAllEntries, createEntry, deleteEntry } from "./db.js"; 6 7 7 8 // In ESM (ES Modules), __dirname doesn't exist like it does in CommonJS. 8 9 // This is a common pattern you'll see everywhere — it reconstructs __dirname ··· 11 12 12 13 const app = new Hono(); 13 14 14 - // Serve the main page 15 - // Hono uses a similar pattern to Rust frameworks like Axum: 16 - // app.get("/path", handlerFunction) 17 - // The `c` parameter is the "Context" — it holds the request and 18 - // gives you methods to build responses. Similar to how Axum handlers 19 - // receive extractors. 15 + // === 16 + // User facing routes 17 + // === 18 + 20 19 app.get("/", (c) => { 21 20 const html = readFileSync(resolve(__dirname, "public/index.html"), "utf-8"); 22 21 return c.html(html); 23 22 }); 24 23 25 - // A simple API route so you can see how JSON responses work. 26 - // We'll replace this with real guestbook routes later. 24 + // === 25 + // API routes 26 + // === 27 + 27 28 app.get("/api/health", (c) => { 28 29 return c.json({ status: "ok", timestamp: new Date().toISOString() }); 29 30 }); 30 31 32 + /** GET /api/entries - return all guest book entries */ 33 + app.get("/api/entries", (c) => { 34 + const entries = getAllEntries(); 35 + return c.json(entries); 36 + }); 37 + 38 + app.post("/api/entries", async (c) => { 39 + const body = await c.req.json(); 40 + 41 + // basic validation, replace later 42 + if (!body.message || typeof body.message !== "string") { 43 + return c.json({ error: "Message is required" }, 400); 44 + } 45 + 46 + if (!body.user_did || !body.user_handle) { 47 + return c.json({ error: "User info is required" }, 400); 48 + } 49 + 50 + const message = body.message.trim().slice(0, 500); 51 + if (message.length === 0) { 52 + return c.json({ error: "Message cannot be empty" }, 400); 53 + } 54 + 55 + const entry = createEntry({ 56 + user_did: body.user_did, 57 + user_handle: body.user_handle, 58 + message, 59 + }); 60 + 61 + 62 + return c.json(entry, 201); 63 + 64 + }); 65 + 66 + app.delete("/api/entries/:id", async (c) => { 67 + const id = Number(c.req.param("id")); 68 + const body = await c.req.json(); 69 + 70 + // Right now I am trusting whatever DID the user provides, this will change after auth is added 71 + 72 + if (!body.user_did) { 73 + return c.json({ error: "User info is required" }, 400); 74 + } 75 + 76 + const deleted = deleteEntry(id, body.user_did); 77 + 78 + if (!deleted) { 79 + return c.json({ error: "Entry not found" }, 404); 80 + } 81 + 82 + return c.body(null, 204); 83 + 84 + }); 85 + 86 + // === 31 87 // Start the server 88 + // === 89 + 32 90 const port = 3000; 33 91 console.log(`Guestbook server running at http://localhost:${port}`); 34 92 serve({ fetch: app.fetch, port });