This commit is contained in:
Julian Freeman
2025-12-01 12:40:26 -04:00
commit 9ace5221fc
10 changed files with 435 additions and 0 deletions

34
src/db.ts Normal file
View File

@@ -0,0 +1,34 @@
import { Database } from "bun:sqlite";
import { mkdirSync } from "node:fs";
import { join } from "node:path";
const DB_DIR = join(process.cwd(), "data");
// Ensure data directory exists
try {
mkdirSync(DB_DIR, { recursive: true });
} catch (e) {
// Ignore if exists
}
const db = new Database(join(DB_DIR, "safe-marks.db"));
export function initDB() {
console.log("Creating database and tables...");
db.run(`
CREATE TABLE IF NOT EXISTS extensions (
ID TEXT PRIMARY KEY,
NAME TEXT NOT NULL,
SAFE INTEGER NOT NULL,
UPDATE_DATE TEXT NOT NULL,
NOTES TEXT
)
`);
// Create index on ID if needed, though PRIMARY KEY implies unique index usually.
// The Python code had index=True on ID.
db.run(`CREATE INDEX IF NOT EXISTS idx_id ON extensions (ID)`);
console.log("Database and tables created successfully (if they didn't exist).");
}
export default db;

191
src/index.ts Normal file
View File

@@ -0,0 +1,191 @@
import { Hono } from "hono";
import db, { initDB } from "./db";
import {
SafeStatus,
type ExtensionCreate,
type ExtensionBatchCreateItem,
type ExtensionUpdatePayload,
type ExtensionInDB,
type ExtensionNecessary,
} from "./models";
// Initialize Database
initDB();
const app = new Hono();
// Helper to get extension by ID
function getExtensionById(id: string): ExtensionInDB | null {
const query = db.query("SELECT * FROM extensions WHERE ID = $id");
return query.get({ $id: id }) as ExtensionInDB | null;
}
// --- API Routes ---
app.get("/api/v1/ext/query_all", (c) => {
const query = db.query("SELECT * FROM extensions");
const extensions = query.all() as ExtensionInDB[];
return c.json(extensions);
});
app.get("/api/v1/ext/query_necessary", (c) => {
// Python code fetches all columns then filters via Pydantic model.
// We can select just what we need or select all and map.
// Let's select all to be consistent with the 'all()' behavior if logic changes,
// but map to the response shape to respect the 'response_model'.
const query = db.query("SELECT * FROM extensions");
const extensions = query.all() as ExtensionInDB[];
const necessary: ExtensionNecessary[] = extensions.map((ext) => ({
ID: ext.ID,
SAFE: ext.SAFE,
}));
return c.json(necessary);
});
app.post("/api/v1/ext/add_one", async (c) => {
const body = await c.req.json<ExtensionCreate>();
// Validation (basic, assuming valid types passed)
if (!body.ID || !body.NAME || body.SAFE === undefined) {
// Python throws 422 for validation errors usually, checking manual checks here
}
if (getExtensionById(body.ID)) {
return c.json({ detail: `Extension with ID ${body.ID} already exists.` }, 400);
}
const currentDate = new Date().toISOString();
const newExtension: ExtensionInDB = {
ID: body.ID,
NAME: body.NAME,
NOTES: body.NOTES ?? null,
SAFE: body.SAFE,
UPDATE_DATE: currentDate,
};
const insert = db.query(`
INSERT INTO extensions (ID, NAME, SAFE, UPDATE_DATE, NOTES)
VALUES ($ID, $NAME, $SAFE, $UPDATE_DATE, $NOTES)
`);
insert.run({
$ID: newExtension.ID,
$NAME: newExtension.NAME,
$SAFE: newExtension.SAFE,
$UPDATE_DATE: newExtension.UPDATE_DATE,
$NOTES: newExtension.NOTES,
});
return c.json(newExtension);
});
app.post("/api/v1/ext/add_batch", async (c) => {
const items = await c.req.json<ExtensionBatchCreateItem[]>();
const createdExtensions: ExtensionInDB[] = [];
const currentDate = new Date().toISOString();
const insert = db.query(`
INSERT INTO extensions (ID, NAME, SAFE, UPDATE_DATE, NOTES)
VALUES ($ID, $NAME, $SAFE, $UPDATE_DATE, $NOTES)
`);
// Use a transaction for batch insert
const insertTransaction = db.transaction((itemsToInsert: ExtensionBatchCreateItem[]) => {
for (const item of itemsToInsert) {
if (getExtensionById(item.ID)) {
continue;
}
const newExtension: ExtensionInDB = {
ID: item.ID,
NAME: item.NAME,
NOTES: item.NOTES ?? null,
SAFE: SafeStatus.unknown,
UPDATE_DATE: currentDate,
};
insert.run({
$ID: newExtension.ID,
$NAME: newExtension.NAME,
$SAFE: newExtension.SAFE,
$UPDATE_DATE: newExtension.UPDATE_DATE,
$NOTES: newExtension.NOTES,
});
createdExtensions.push(newExtension);
}
});
insertTransaction(items);
return c.json(createdExtensions);
});
app.put("/api/v1/ext/update_one/:item_id", async (c) => {
const itemId = c.req.param("item_id");
const updateData = await c.req.json<ExtensionUpdatePayload>();
const currentExt = getExtensionById(itemId);
if (!currentExt) {
return c.json({ detail: `Extension with ID ${itemId} not found.` }, 404);
}
if (Object.keys(updateData).length === 0) {
return c.json({ detail: "No update data provided." }, 400);
}
let updated = false;
const nextExt = { ...currentExt };
if (updateData.NAME !== undefined && updateData.NAME !== null && updateData.NAME !== currentExt.NAME) {
nextExt.NAME = updateData.NAME;
updated = true;
}
if (updateData.SAFE !== undefined && updateData.SAFE !== null && updateData.SAFE !== currentExt.SAFE) {
nextExt.SAFE = updateData.SAFE;
updated = true;
}
if (updateData.NOTES !== undefined && updateData.NOTES !== null && updateData.NOTES !== currentExt.NOTES) {
nextExt.NOTES = updateData.NOTES;
updated = true;
}
if (updated) {
nextExt.UPDATE_DATE = new Date().toISOString();
const updateQuery = db.query(`
UPDATE extensions
SET NAME = $NAME, SAFE = $SAFE, NOTES = $NOTES, UPDATE_DATE = $UPDATE_DATE
WHERE ID = $ID
`);
updateQuery.run({
$NAME: nextExt.NAME,
$SAFE: nextExt.SAFE,
$NOTES: nextExt.NOTES,
$UPDATE_DATE: nextExt.UPDATE_DATE,
$ID: nextExt.ID,
});
}
return c.json(nextExt);
});
app.delete("/api/v1/ext/delete_one/:item_id", (c) => {
const itemId = c.req.param("item_id");
const currentExt = getExtensionById(itemId);
if (!currentExt) {
return c.json({ detail: `Extension with ID ${itemId} not found.` }, 404);
}
const deleteQuery = db.query("DELETE FROM extensions WHERE ID = $id");
deleteQuery.run({ $id: itemId });
return c.json({ message: `Extension with ID ${itemId} successfully deleted.` });
});
export default app;

34
src/models.ts Normal file
View File

@@ -0,0 +1,34 @@
export enum SafeStatus {
safe = 1,
unsure = 0,
unsafe = -1,
unknown = -2,
}
export interface ExtensionBase {
ID: string;
NAME: string;
NOTES?: string | null;
}
export interface ExtensionCreate extends ExtensionBase {
SAFE: SafeStatus;
}
export interface ExtensionBatchCreateItem extends ExtensionBase {}
export interface ExtensionUpdatePayload {
NAME?: string | null;
SAFE?: SafeStatus | null;
NOTES?: string | null;
}
export interface ExtensionInDB extends ExtensionBase {
SAFE: SafeStatus;
UPDATE_DATE: string;
}
export interface ExtensionNecessary {
ID: string;
SAFE: SafeStatus;
}