first
This commit is contained in:
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal 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
15
README.md
Normal 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
31
bun.lock
Normal 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
6
index.ts
Normal 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
15
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
45
spec/MIGRATE_INSTRUCTIONS.md
Normal file
45
spec/MIGRATE_INSTRUCTIONS.md
Normal 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
34
src/db.ts
Normal 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
191
src/index.ts
Normal 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
34
src/models.ts
Normal 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
29
tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user