Compare commits

..
2 Commits
Author SHA1 Message Date
Nicholas Keller 85e2eaf1ca publishing2 2026-07-28 10:57:02 -04:00
Nicholas Keller a47eb13b62 publishing 2026-07-28 10:56:50 -04:00
15 changed files with 218 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
SPARK_PORT=7654
DB_FILE_NAME=./spark.db
+5
View File
@@ -0,0 +1,5 @@
node_modules
bun.lock
spark.db
.DS_Store
.env
+10
View File
@@ -0,0 +1,10 @@
import {defineConfig} from "drizzle-kit";
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema.ts",
dialect: "sqlite",
dbCredentials: {
url: process.env.DB_FILE_NAME!
}
});
+20
View File
@@ -0,0 +1,20 @@
{
"dependencies": {
"@libsql/client": "^0.17.3",
"bun": "^1.3.13",
"drizzle-orm": "^0.45.2",
"express": "^5.2.1",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^25.6.2",
"bun-types": "^1.3.13",
"drizzle-kit": "^0.31.10"
},
"main": "src/index.ts",
"scripts": {
"start": "bun run src/index.ts",
"dev": "bun run --watch src/index.ts"
}
}
+3
View File
@@ -0,0 +1,3 @@
import { drizzle } from "drizzle-orm/bun-sqlite";
import path from "node:path";
export default drizzle(path.join(__dirname, "../..", process.env.DB_FILE_NAME!));
+12
View File
@@ -0,0 +1,12 @@
import { sql } from "drizzle-orm";
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const ideasTable = sqliteTable("ideas", {
id: int().primaryKey({autoIncrement: true}),
createdAt: int("created_at", {mode: "timestamp"}).default(sql`(unixepoch())`).notNull(),
updatedAt: int("updated_at", {mode: "timestamp"}).default(sql`(unixepoch())`).notNull(),
name: text().notNull(),
description: text(),
notes: text()
})
+10
View File
@@ -0,0 +1,10 @@
import { eq } from "drizzle-orm";
import db from "../db";
import { ideasTable } from "../db/schema";
import { Request, Response } from "express";
export default async function deleteIdeaHandler(req: Request, res: Response) {
const { id } = req.params;
await db.delete(ideasTable).where(eq(ideasTable.id, Number(id)));
res.json({message: "Idea deleted"})
}
+10
View File
@@ -0,0 +1,10 @@
import db from "../db";
import { ideasTable } from "../db/schema";
import { Request, Response } from "express";
import { eq } from "drizzle-orm";
export default async function getIdeaHandler(req: Request, res: Response) {
const { id } = req.params;
const idea = await db.select().from(ideasTable).where(eq(ideasTable.id, Number(id)));
res.json(idea);
}
+8
View File
@@ -0,0 +1,8 @@
import db from "../db";
import { ideasTable } from "../db/schema";
import { Request, Response } from "express";
export default async function getIdeasHandler(req: Request, res: Response) {
const ideas = await db.select().from(ideasTable);
res.json(ideas);
}
+15
View File
@@ -0,0 +1,15 @@
import z from "zod";
import { newIdeaSchema, TypedRequest } from "../schemas";
import { Response } from "express";
import db from "../db";
import { ideasTable } from "../db/schema";
export default async function newIdeaHandler(req: TypedRequest<z.output<typeof newIdeaSchema>>, res: Response) {
const {name, description, notes} = req.body;
const [{id}] = await db.insert(ideasTable).values({
name: name || "",
description: description || "",
notes: notes || ""
}).returning({id: ideasTable.id })
res.status(201).json({message: "Idea created", id});
}
+13
View File
@@ -0,0 +1,13 @@
import { Request, Response } from "express";
import db from "../db";
import { ideasTable } from "../db/schema";
export default async function previewIdeasHandler(req: Request, res: Response) {
const ideas = await db.select({
id: ideasTable.id,
name: ideasTable.name,
description: ideasTable.description,
updatedAt: ideasTable.updatedAt,
}).from(ideasTable);
res.json(ideas);
}
+24
View File
@@ -0,0 +1,24 @@
import { Response } from "express";
import db from "../db";
import { ideasTable } from "../db/schema";
import { eq, sql } from "drizzle-orm";
import { TypedRequest, updateIdeaSchema } from "../schemas";
import z from "zod";
export default async function updateIdeaHandler(req: TypedRequest<z.output<typeof updateIdeaSchema>>, res: Response) {
let {id} = req.params;
const idParseResult = z.number().int().safeParse(Number(id));
if (!idParseResult.success) {
res.status(400).json({message: "Invalid idea ID"});
return;
}
id = idParseResult.data;
const {name, description, notes} = req.body;
await db.update(ideasTable).set({
...(name !== undefined ? { name } : {}),
...(description !== undefined ? { description } : {}),
...(notes !== undefined ? { notes } : {}),
updatedAt: sql`(unixepoch())`
}).where(eq(ideasTable.id, Number(id)))
res.json({message: "Idea updated", id});
}
+24
View File
@@ -0,0 +1,24 @@
import express from "express"
import { newIdeaSchema, updateIdeaSchema, validateSchema } from "./schemas";
import newIdeaHandler from "./handlers/newIdea";
import getIdeasHandler from "./handlers/getIdeas";
import getIdeaHandler from "./handlers/getIdea";
import previewIdeasHandler from "./handlers/previewIdeas";
import deleteIdeaHandler from "./handlers/deleteIdea";
import updateIdeaHandler from "./handlers/updateIdea";
const spark = express();
spark.use(express.json());
spark.post("/idea", validateSchema(newIdeaSchema), newIdeaHandler);
spark.get("/idea", getIdeasHandler);
spark.get("/idea/preview", previewIdeasHandler);
spark.get("/idea/:id", getIdeaHandler);
spark.patch("/idea/:id", validateSchema(updateIdeaSchema), updateIdeaHandler);
spark.delete("/idea/:id", deleteIdeaHandler);
const PORT: number = Number(process.env.SPARK_PORT || 7654);
spark.listen(PORT, () => {
console.log(`Listening on port ${PORT}`)
})
+55
View File
@@ -0,0 +1,55 @@
import { NextFunction, RequestHandler, Response } from "express";
import z from "zod";
//#region Credit to Galtzabari
export interface TypedRequest<T> extends Express.Request {
// allow any params shape to be compatible with Express's ParamsDictionary
params: Record<string, any>;
body: T;
}
export const validateSchema = <T extends z.ZodTypeAny>(schema: T, idRequired: boolean = false): RequestHandler => {
return (
req: TypedRequest<z.output<T>>,
res: Response,
next: NextFunction
): void => {
const validationResult = schema.safeParse(req.body);
if (!validationResult.success) {
res.status(400).json({
message: "Validation failed",
errors: validationResult.error!.issues,
});
return;
}
if (idRequired) {
const idValidationResult = z.number().int().safeParse(req.params.id);
if (!idValidationResult.success) {
res.status(400).json({
message: "Invalid ID parameter",
errors: idValidationResult.error.issues,
});
return;
}
}
// Overwrite the req.body with validated data
req.body = validationResult.data;
next();
};
};
//#endregion
export const newIdeaSchema = z.object({
name: z.string().optional(),
description: z.string().optional(),
notes: z.string().optional()
})
export const updateIdeaSchema = z.object({
id: z.number().int(),
name: z.string().optional(),
description: z.string().optional(),
notes: z.string().optional()
})
+7
View File
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"types": [
"bun-types"
]
}
}