diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b4bc904 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +SPARK_PORT=7654 +DB_FILE_NAME=./spark.db \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c7e3bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +bun.lock +spark.db +.DS_Store +.env \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..7a5ab96 --- /dev/null +++ b/drizzle.config.ts @@ -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! + } +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ca0bf8d --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..2245bb0 --- /dev/null +++ b/src/db/index.ts @@ -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!)); diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..73014f3 --- /dev/null +++ b/src/db/schema.ts @@ -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() +}) diff --git a/src/handlers/getIdeas.ts b/src/handlers/getIdeas.ts new file mode 100644 index 0000000..76e0948 --- /dev/null +++ b/src/handlers/getIdeas.ts @@ -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); +} \ No newline at end of file diff --git a/src/handlers/newIdea.ts b/src/handlers/newIdea.ts new file mode 100644 index 0000000..e6196fe --- /dev/null +++ b/src/handlers/newIdea.ts @@ -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>, res: Response) { + const {name, description, notes} = req.body; + await db.insert(ideasTable).values({ + name: name || "", + description: description || "", + notes: notes || "" + }) + res.status(201).json({message: "Idea created"}) +} \ No newline at end of file diff --git a/src/handlers/previewIdeas.ts b/src/handlers/previewIdeas.ts new file mode 100644 index 0000000..ad141bf --- /dev/null +++ b/src/handlers/previewIdeas.ts @@ -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); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1542104 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,17 @@ +import express from "express" +import { newIdeaSchema, validateSchema } from "./schemas"; +import newIdeaHandler from "./handlers/newIdea"; +import getIdeasHandler from "./handlers/getIdeas"; +import previewIdeasHandler from "./handlers/previewIdeas"; +const spark = express(); + +spark.use(express.json()); + +spark.post("/idea", validateSchema(newIdeaSchema), newIdeaHandler); +spark.get("/idea", getIdeasHandler); +spark.get("/idea/preview", previewIdeasHandler); + +const PORT: number = Number(process.env.SPARK_PORT || 7654); +spark.listen(PORT, () => { + console.log(`Listening on port ${PORT}`) +}) diff --git a/src/schemas.ts b/src/schemas.ts new file mode 100644 index 0000000..2b342a3 --- /dev/null +++ b/src/schemas.ts @@ -0,0 +1,36 @@ +import { NextFunction, RequestHandler, Response } from "express"; +import z from "zod"; + +//#region Credit to Galtzabari +export interface TypedRequest extends Express.Request { + body: T; +} +export const validateSchema = (schema: T): RequestHandler => { + return ( + req: TypedRequest>, + 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; + } + + // 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() +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3827dc9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "types": [ + "bun-types" + ] + } +} \ No newline at end of file