import { serve } from "@hono/node-server"; import { Hono } from "hono"; import { showRoutes } from "hono/dev"; import { DatabaseSync, SQLInputValue } from "node:sqlite"; import { componentColumns, locationColumns, movementColumns, movementComponentColumns, projectColumns, projectComponentColumns } from "../shared/types"; const app = new Hono(); const api = new Hono().basePath("/api"); const db = new DatabaseSync("./inventory.sqlite"); const sql = db.createTagStore(); sql.run`PRAGMA foreign_keys = ON`; const wrapWithTransaction = (runnable: () => T): T => { try { sql.run`BEGIN`; const result = runnable(); sql.run`COMMIT`; return result; } catch (e) { sql.run`ROLLBACK`; throw e; } }; const insertHelper = (table: string, columns: K[], data: Record) => { return db.prepare(` INSERT INTO ${table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")}) `).run(...columns.map(k => data[k] ?? null)); }; const patchHelper = (table: string, columns: K[], id: number, data: Partial>) => { const validEntries = Object.entries(data).filter(([k]) => columns.includes(k as K)); if (validEntries.length === 0) throw new Error("Nothing to update"); return db.prepare(` UPDATE ${table} SET ${validEntries.map(([k]) => `${k} = ?`).join(", ")} WHERE id = ? `).run(...validEntries.map(([, v]) => v as any), id); }; const tables: [string, string[]][] = [ [ "location", locationColumns ], [ "component", componentColumns ], [ "project", projectColumns ], [ "project_component", projectComponentColumns ], [ "movement", movementColumns ], [ "movement_component", movementComponentColumns ], ]; for (const [ tableName, columns ] of tables) { const path = `/${tableName.replace(/_(.)/g, (_, v) => v.toUpperCase())}`; api.get(`${path}`, c => { const { limit = "50", offset = "0" } = c.req.query(); const parsedLimit = Math.max(0, Number(limit) || 50); const parsedOffset = Math.max(0, Number(offset) || 50); const stmt = db.prepare(`SELECT * FROM ${tableName} LIMIT ? OFFSET ?`); const countStmt = db.prepare(`SELECT COUNT(*) AS count FROM ${tableName}`); return c.json({ data: stmt.all(parsedLimit, parsedOffset), limit: parsedLimit, offset: parsedOffset, count: countStmt.get()!.count, }); }); api.post(`${path}`, async c => { const data = await c.req.json(); const { lastInsertRowid } = insertHelper(tableName, columns, data); return c.json({ id: lastInsertRowid }); }); api.get(`${path}/:id`, c => { const id = c.req.param("id"); const stmt = db.prepare(`SELECT * FROM ${tableName} WHERE id = ?`); return c.json(stmt.get(id)); }); api.patch(`${path}/:id`, async c => { const id = parseInt(c.req.param("id")); const data = await c.req.json(); patchHelper(tableName, columns, id, data); return c.json({}); }); } { sql.run`CREATE TABLE IF NOT EXISTS version ( version INTEGER NOT NULL ) STRICT`; let version = sql.get`SELECT version FROM version LIMIT 1`?.version; console.log(`Current database version: ${version}`); if (version === undefined) { wrapWithTransaction(() => { sql.run`CREATE TABLE location ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, cabinet TEXT, drawer TEXT, bin TEXT ) STRICT`; sql.run`CREATE TABLE component ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, partNumber TEXT NOT NULL, manufacturer TEXT, description TEXT, packaging TEXT, category TEXT, package TEXT, value TEXT, quantity INTEGER NOT NULL, unit TEXT NOT NULL, location INTEGER, datasheet TEXT, FOREIGN KEY (location) REFERENCES location(id) ) STRICT`; sql.run`CREATE INDEX component_location_idx ON component(location)`; sql.run`CREATE TABLE project ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL ) STRICT`; sql.run`CREATE TABLE project_component ( project INTEGER NOT NULL, component INTEGER NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0), PRIMARY KEY (project, component), FOREIGN KEY (component) REFERENCES component(id), FOREIGN KEY (project) REFERENCES project(id) ) STRICT`; sql.run`CREATE INDEX project_component_component_idx ON project_component(component)`; sql.run`CREATE TABLE movement ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, date INTEGER, reason TEXT, project INTEGER, FOREIGN KEY (project) REFERENCES project(id) ) STRICT`; sql.run`CREATE INDEX movement_project_idx ON movement(project)`; sql.run`CREATE TABLE movement_component ( movement INTEGER NOT NULL, component INTEGER NOT NULL, quantity INTEGER NOT NULL, price INTEGER, PRIMARY KEY (movement, component), FOREIGN KEY (movement) REFERENCES movement(id), FOREIGN KEY (component) REFERENCES component(id) ) STRICT`; sql.run`CREATE INDEX movement_component_component_idx ON movement_component(component)`; sql.run`CREATE INDEX movement_component_movement_idx ON movement_component(movement)`; // language=SQLite - no idea why it's not doing that automatically here sql.run`INSERT INTO version (version) VALUES (1)`; console.log("initialized database to version 1"); version = 1; }); } } app.route("/", api); showRoutes(app); serve({ fetch: app.fetch, port: Number(process.env.PORT ?? 3000), }, ({ port }) => { console.log(`Listening on port ${port}`); });