170 lines
5.6 KiB
TypeScript
170 lines
5.6 KiB
TypeScript
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 = <T>(runnable: () => T): T => {
|
|
try {
|
|
sql.run`BEGIN`;
|
|
const result = runnable();
|
|
sql.run`COMMIT`;
|
|
return result;
|
|
} catch (e) {
|
|
sql.run`ROLLBACK`;
|
|
throw e;
|
|
}
|
|
};
|
|
const insertHelper = <K extends PropertyKey>(table: string, columns: K[], data: Record<K, SQLInputValue>) => {
|
|
return db.prepare(`
|
|
INSERT INTO ${table} (${columns.join(", ")})
|
|
VALUES (${columns.map(() => "?").join(", ")})
|
|
`).run(...columns.map(k => data[k] ?? null));
|
|
};
|
|
const patchHelper = <K extends PropertyKey>(table: string, columns: K[], id: number, data: Partial<Record<K, SQLInputValue>>) => {
|
|
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}`);
|
|
});
|