commit 9ace5221fc3888f5029ea738bd64d002f2b88886 Author: Julian Freeman Date: Mon Dec 1 12:40:26 2025 -0400 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f97ad2 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1bb0ec --- /dev/null +++ b/README.md @@ -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. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..f5a2e18 --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..6ab2a89 --- /dev/null +++ b/index.ts @@ -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, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..fbe4822 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/spec/MIGRATE_INSTRUCTIONS.md b/spec/MIGRATE_INSTRUCTIONS.md new file mode 100644 index 0000000..06e9f15 --- /dev/null +++ b/spec/MIGRATE_INSTRUCTIONS.md @@ -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.** \ No newline at end of file diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 0000000..f320943 --- /dev/null +++ b/src/db.ts @@ -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; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2a43ee1 --- /dev/null +++ b/src/index.ts @@ -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(); + + // 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(); + 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(); + + 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; diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 0000000..04722c5 --- /dev/null +++ b/src/models.ts @@ -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; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -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 + } +}