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

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
data/

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# safemarks-server
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.3.3. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

31
bun.lock Normal file
View File

@@ -0,0 +1,31 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "safemarks-server",
"dependencies": {
"hono": "^4.10.7",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

6
index.ts Normal file
View File

@@ -0,0 +1,6 @@
import app from "./src/index";
export default {
port: 3000, // Or whatever port you prefer, or Bun's default
fetch: app.fetch,
};

15
package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "safemarks-server",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"hono": "^4.10.7"
}
}

View File

@@ -0,0 +1,45 @@
# Project Migration Task: Python/FastAPI to Bun/Hono
## 1. Role & Objective
Act as a Senior Backend Engineer expert in both Python (FastAPI) and the modern JavaScript/TypeScript ecosystem (Bun runtime).
**Your Goal:** Rewrite the existing API service located in the `python-ref/` directory using **Bun**, **Hono**, and **SQLite**.
## 2. Source Code Context
The source code is located in the `python-ref/` folder. It currently runs on:
- Python 3.x
- FastAPI
- SQLite3 (standard library)
## 3. Target Technology Stack
You must use the following stack for the rewrite:
- **Runtime:** Bun (latest version)
- **Framework:** Hono (latest version)
- **Database:** Native `bun:sqlite` (do not use generic node-sqlite3 unless necessary, prefer Bun's native high-performance driver).
- **Language:** TypeScript
## 4. Strict Requirements
The new service must be a "drop-in" replacement. Adhere to the following rules strictly:
### A. Database Integrity
1. Analyze the SQL schemas or ORM models in `python-ref`.
2. Recreate the **exact same** database table structures (table names, column names, data types, constraints).
3. If the Python code uses an ORM (like SQLAlchemy or Tortoise), translate the logic to raw SQL or a lightweight query builder suitable for `bun:sqlite`.
### B. API Consistency
1. Keep **all API URL paths** exactly the same (e.g., if Python has `/api/v1/users`, Hono must have `/api/v1/users`).
2. Keep **HTTP Methods** exactly the same (GET, POST, PUT, DELETE).
3. Keep **Request/Response JSON formats** exactly the same. Field names must not change (e.g., do not change `user_id` to `userId` in the JSON response, keep it consistent with the old API).
### C. Code Structure
1. Organize the code logically (e.g., separate `db.ts` for database connections, `index.ts` for routes).
2. Use TypeScript interfaces to define the shape of Request/Response bodies.
## 5. Deliverables
Please read the files in `python-ref/` recursively. Then, generate the following:
1. **Project Setup:** The `package.json` command or content needed to install dependencies.
2. **Database Setup:** A script or function to initialize the SQLite database matching the old schema.
3. **Application Code:** The full source code for the Hono application.
**Start by analyzing the key files in `python-ref/` and summarizing the endpoints and schema you found, then proceed to write the code.**

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;
}

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}