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