This commit is contained in:
Nicholas Keller
2026-06-09 23:48:27 -04:00
commit 6cf48a9a9e
12 changed files with 1256 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
import { Database } from "bun:sqlite";
export const db = new Database("spark_slop.db", { create: true });
// Use WAL for better concurrent read/write performance.
db.exec("PRAGMA journal_mode = WAL;");
db.exec("PRAGMA foreign_keys = ON;");
db.exec(`
CREATE TABLE IF NOT EXISTS ideas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
idea_id INTEGER NOT NULL,
body TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (idea_id) REFERENCES ideas(id) ON DELETE CASCADE
);
`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_notes_idea_id ON notes(idea_id);`);
export interface Idea {
id: number;
title: string;
description: string;
created_at: string;
updated_at: string;
note_count?: number;
}
export interface Note {
id: number;
idea_id: number;
body: string;
created_at: string;
}
// --- Idea queries ---
export function listIdeas(): Idea[] {
return db
.query(
`SELECT i.*, COUNT(n.id) AS note_count
FROM ideas i
LEFT JOIN notes n ON n.idea_id = i.id
GROUP BY i.id
ORDER BY i.updated_at DESC`
)
.all() as Idea[];
}
export function getIdea(id: number): Idea | null {
return (db.query(`SELECT * FROM ideas WHERE id = ?`).get(id) as Idea) ?? null;
}
export function createIdea(title: string, description: string): Idea {
const row = db
.query(
`INSERT INTO ideas (title, description) VALUES (?, ?) RETURNING *`
)
.get(title, description) as Idea;
return row;
}
export function updateIdea(
id: number,
title: string,
description: string
): Idea | null {
return (
(db
.query(
`UPDATE ideas
SET title = ?, description = ?, updated_at = datetime('now')
WHERE id = ?
RETURNING *`
)
.get(title, description, id) as Idea) ?? null
);
}
export function deleteIdea(id: number): boolean {
const res = db.query(`DELETE FROM ideas WHERE id = ?`).run(id);
return res.changes > 0;
}
// --- Note queries ---
export function listNotes(ideaId: number): Note[] {
return db
.query(`SELECT * FROM notes WHERE idea_id = ? ORDER BY created_at ASC, id ASC`)
.all(ideaId) as Note[];
}
export function createNote(ideaId: number, body: string): Note {
const note = db
.query(`INSERT INTO notes (idea_id, body) VALUES (?, ?) RETURNING *`)
.get(ideaId, body) as Note;
// Touch the parent idea so it sorts to the top.
db.query(`UPDATE ideas SET updated_at = datetime('now') WHERE id = ?`).run(
ideaId
);
return note;
}
export function deleteNote(id: number): boolean {
const res = db.query(`DELETE FROM notes WHERE id = ?`).run(id);
return res.changes > 0;
}
+138
View File
@@ -0,0 +1,138 @@
import {
listIdeas,
getIdea,
createIdea,
updateIdea,
deleteIdea,
listNotes,
createNote,
deleteNote,
} from "./db";
const PORT = Number(process.env.PORT ?? 3000);
const PUBLIC_DIR = new URL("../public/", import.meta.url).pathname;
function json(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { "Content-Type": "application/json" },
});
}
function err(message: string, status = 400): Response {
return json({ error: message }, status);
}
async function readJson(req: Request): Promise<any> {
try {
return await req.json();
} catch {
return {};
}
}
async function serveStatic(pathname: string): Promise<Response> {
// Map "/" -> index.html, "/app" -> app.html, otherwise the literal file.
let rel = pathname === "/" ? "index.html" : pathname.slice(1);
if (rel === "app") rel = "app.html";
let file = Bun.file(PUBLIC_DIR + rel);
if (!(await file.exists())) {
// SPA-ish fallback for unknown non-asset routes.
if (!rel.includes(".")) file = Bun.file(PUBLIC_DIR + "index.html");
}
if (!(await file.exists())) return new Response("Not found", { status: 404 });
return new Response(file);
}
const server = Bun.serve({
port: PORT,
async fetch(req) {
const url = new URL(req.url);
const { pathname } = url;
// ---------- API ----------
if (pathname.startsWith("/api/")) {
try {
return await handleApi(req, pathname);
} catch (e) {
console.error(e);
return err("Internal server error", 500);
}
}
// ---------- Static ----------
return serveStatic(pathname);
},
});
async function handleApi(req: Request, pathname: string): Promise<Response> {
const method = req.method;
// /api/ideas
if (pathname === "/api/ideas") {
if (method === "GET") return json(listIdeas());
if (method === "POST") {
const { title, description } = await readJson(req);
if (!title || typeof title !== "string" || !title.trim())
return err("Title is required");
return json(
createIdea(title.trim(), (description ?? "").toString().trim()),
201
);
}
return err("Method not allowed", 405);
}
// /api/ideas/:id
let m = pathname.match(/^\/api\/ideas\/(\d+)$/);
if (m) {
const id = Number(m[1]);
if (method === "GET") {
const idea = getIdea(id);
return idea ? json(idea) : err("Idea not found", 404);
}
if (method === "PUT") {
const { title, description } = await readJson(req);
if (!title || !title.trim()) return err("Title is required");
const updated = updateIdea(
id,
title.trim(),
(description ?? "").toString().trim()
);
return updated ? json(updated) : err("Idea not found", 404);
}
if (method === "DELETE") {
return deleteIdea(id) ? json({ ok: true }) : err("Idea not found", 404);
}
return err("Method not allowed", 405);
}
// /api/ideas/:id/notes
m = pathname.match(/^\/api\/ideas\/(\d+)\/notes$/);
if (m) {
const ideaId = Number(m[1]);
if (!getIdea(ideaId)) return err("Idea not found", 404);
if (method === "GET") return json(listNotes(ideaId));
if (method === "POST") {
const { body } = await readJson(req);
if (!body || !body.toString().trim()) return err("Note body is required");
return json(createNote(ideaId, body.toString().trim()), 201);
}
return err("Method not allowed", 405);
}
// /api/notes/:id
m = pathname.match(/^\/api\/notes\/(\d+)$/);
if (m) {
const id = Number(m[1]);
if (method === "DELETE") {
return deleteNote(id) ? json({ ok: true }) : err("Note not found", 404);
}
return err("Method not allowed", 405);
}
return err("Not found", 404);
}
console.log(`✨ Spark Slop running at http://localhost:${server.port}`);