Browse Source

feat: min usable version

kotoyuuko 1 week ago
parent
commit
10ce4f58e8
6 changed files with 178 additions and 44 deletions
  1. 6 0
      .prettierrc
  2. 52 0
      eslint.config.js
  3. 16 2
      package.json
  4. 96 4
      src/index.ts
  5. 3 5
      tsconfig.json
  6. 5 33
      wrangler.jsonc

+ 6 - 0
.prettierrc

@@ -0,0 +1,6 @@
+{
+  "arrowParens": "avoid",
+  "semi": false,
+  "trailingComma": "es5",
+  "endOfLine": "lf"
+}

+ 52 - 0
eslint.config.js

@@ -0,0 +1,52 @@
+const { defineConfig } = require("eslint/config")
+
+const tsParser = require("@typescript-eslint/parser")
+const typescriptEslint = require("@typescript-eslint/eslint-plugin")
+const globals = require("globals")
+const js = require("@eslint/js")
+
+const { FlatCompat } = require("@eslint/eslintrc")
+
+const compat = new FlatCompat({
+  baseDirectory: __dirname,
+  recommendedConfig: js.configs.recommended,
+  allConfig: js.configs.all,
+})
+
+module.exports = defineConfig([
+  {
+    languageOptions: {
+      parser: tsParser,
+
+      globals: {
+        ...globals.node,
+        ...globals.commonjs,
+      },
+    },
+
+    plugins: {
+      "@typescript-eslint": typescriptEslint,
+    },
+
+    extends: compat.extends(
+      "eslint:recommended",
+      "plugin:@typescript-eslint/eslint-recommended",
+      "plugin:@typescript-eslint/recommended"
+    ),
+
+    rules: {
+      "@typescript-eslint/no-unused-vars": [
+        "error",
+        {
+          args: "all",
+          argsIgnorePattern: "^_",
+          varsIgnorePattern: "^_assert",
+          caughtErrors: "none",
+          ignoreRestSiblings: true,
+        },
+      ],
+
+      "no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
+    },
+  },
+])

+ 16 - 2
package.json

@@ -3,12 +3,26 @@
   "scripts": {
     "dev": "wrangler dev",
     "deploy": "wrangler deploy --minify",
-    "cf-typegen": "wrangler types --env-interface CloudflareBindings"
+    "cf-typegen": "wrangler types --env-interface CloudflareBindings",
+    "prettier": "prettier --write .",
+    "lint": "prettier --check . && eslint . --ext .ts && cspell '**/*' "
   },
   "dependencies": {
+    "@hono/zod-validator": "^0.7.0",
+    "@notionhq/client": "^3.1.3",
     "hono": "^4.7.11"
   },
   "devDependencies": {
+    "@cloudflare/workers-types": "^4.20250603.0",
+    "@eslint/eslintrc": "^3.3.1",
+    "@eslint/js": "^9.28.0",
+    "@types/node": "^22.15.29",
+    "@typescript-eslint/eslint-plugin": "^8.33.1",
+    "@typescript-eslint/parser": "^8.33.1",
+    "eslint": "^9.28.0",
+    "globals": "^16.2.0",
+    "prettier": "^3.5.3",
+    "typescript": "^5.8.3",
     "wrangler": "^4.4.0"
   }
-}
+}

+ 96 - 4
src/index.ts

@@ -1,9 +1,101 @@
-import { Hono } from 'hono'
+import { Hono } from "hono"
+import { Client, isFullPage } from "@notionhq/client"
+import { z } from "zod"
+import { zValidator } from "@hono/zod-validator"
 
-const app = new Hono()
+type Bindings = {
+  NOTION_KEY: string
+  NOTION_DB_ID: string
+}
 
-app.get('/', (c) => {
-  return c.text('Hello Hono!')
+const xRateQuerySchema = z.object({
+  from: z
+    .string()
+    .min(3, "Currency code must at length 3.")
+    .max(3, "Currency code must at length 3."),
+  to: z
+    .string()
+    .min(3, "Currency code must at length 3.")
+    .max(3, "Currency code must at length 3."),
+  amount: z.string(),
 })
 
+const app = new Hono<{ Bindings: Bindings }>()
+
+app.get(
+  "/convert/:from/:to/:amount",
+  zValidator("param", xRateQuerySchema, (result, c) => {
+    if (!result.success) {
+      return c.json({ error: result.error.errors }, 400)
+    }
+  }),
+  async c => {
+    const notionKey = c.env.NOTION_KEY
+    const notionDatabaseId = c.env.NOTION_DB_ID
+
+    if (!notionDatabaseId || !notionKey) {
+      return c.json({ error: "Missing environment variables." }, 500)
+    }
+
+    const from = c.req.param("from")
+    const to = c.req.param("to")
+    const amount = parseFloat(c.req.param("amount"))
+
+    try {
+      const client = new Client({ auth: notionKey, fetch: fetch.bind(globalThis) })
+      const query = await client.databases.query({
+        database_id: notionDatabaseId,
+        page_size: 2,
+        filter: {
+          or: [
+            {
+              type: "title",
+              property: "Code",
+              title: {
+                equals: from,
+              },
+            },
+            {
+              type: "title",
+              property: "Code",
+              title: {
+                equals: to,
+              },
+            },
+          ],
+        },
+      })
+      const pages = query.results.filter(page => isFullPage(page))
+
+      let ratesMap: {
+        [key: string]: number
+      } = {}
+      for (const page of pages) {
+        let currencyCodeProp = page.properties["Code"]
+        let rateProp = page.properties["Rate"]
+        if (currencyCodeProp!.type !== "title") {
+          continue
+        }
+        if (rateProp!.type !== "number") {
+          continue
+        }
+        ratesMap[currencyCodeProp.title[0]!.plain_text] = rateProp.number!
+      }
+
+      return c.json({
+        rates: ratesMap,
+        amount: amount,
+        target: amount * ratesMap[to] / ratesMap[from]
+      }, 200, {
+        "Content-Type": "application/json; charset=utf-8",
+        "Cache-Control": "s-maxage=300, stale-while-revalidate=60",
+        "Access-Control-Allow-Origin": "*",
+      })
+    } catch (error: any) {
+      console.log(error)
+      return c.json({ error: error.message }, 500)
+    }
+  }
+)
+
 export default app

+ 3 - 5
tsconfig.json

@@ -5,10 +5,8 @@
     "moduleResolution": "Bundler",
     "strict": true,
     "skipLibCheck": true,
-    "lib": [
-      "ESNext"
-    ],
+    "lib": ["ESNext"],
     "jsx": "react-jsx",
     "jsxImportSource": "hono/jsx"
-  },
-}
+  }
+}

+ 5 - 33
wrangler.jsonc

@@ -2,37 +2,9 @@
   "$schema": "node_modules/wrangler/config-schema.json",
   "name": "xrates-queryer",
   "main": "src/index.ts",
-  "compatibility_date": "2025-06-03"
-  // "compatibility_flags": [
-  //   "nodejs_compat"
-  // ],
-  // "vars": {
-  //   "MY_VAR": "my-variable"
-  // },
-  // "kv_namespaces": [
-  //   {
-  //     "binding": "MY_KV_NAMESPACE",
-  //     "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
-  //   }
-  // ],
-  // "r2_buckets": [
-  //   {
-  //     "binding": "MY_BUCKET",
-  //     "bucket_name": "my-bucket"
-  //   }
-  // ],
-  // "d1_databases": [
-  //   {
-  //     "binding": "MY_DB",
-  //     "database_name": "my-database",
-  //     "database_id": ""
-  //   }
-  // ],
-  // "ai": {
-  //   "binding": "AI"
-  // },
-  // "observability": {
-  //   "enabled": true,
-  //   "head_sampling_rate": 1
-  // }
+  "compatibility_date": "2025-06-03",
+  "vars": {
+    "NOTION_KEY": "",
+    "NOTION_DB_ID": "",
+  },
 }