From 3e06d631ac644ec8cd8ead71bbfe879d4d73564d Mon Sep 17 00:00:00 2001 From: Call Vin Date: Thu, 2 Jan 2025 22:58:56 +0700 Subject: [PATCH 01/38] add: [IDE] config --- .idea/.gitignore | 8 ++++++++ .idea/material_theme_project_new.xml | 12 ++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/material_theme_project_new.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml new file mode 100644 index 0000000..bf2d968 --- /dev/null +++ b/.idea/material_theme_project_new.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file From f3e46d9945c6bae524c9c7d56df0d3f1ab6a061f Mon Sep 17 00:00:00 2001 From: Call Vin Date: Thu, 2 Jan 2025 23:00:17 +0700 Subject: [PATCH 02/38] fix: update .gitignore --- .gitignore | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 506e4c3..6a7d6d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,130 @@ -# deps +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file From 9366c785ab3b878a072fb9018b41e55c6f115f8a Mon Sep 17 00:00:00 2001 From: Call Vin Date: Thu, 2 Jan 2025 23:00:34 +0700 Subject: [PATCH 03/38] add: [IDE] config --- .env.example | 7 +++++++ .prettierignore | 2 ++ .prettierrc.json | 9 +++++++++ 3 files changed, 18 insertions(+) create mode 100644 .env.example create mode 100644 .prettierignore create mode 100644 .prettierrc.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3a1a846 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +DATABASE_URL="mysql://root:root@localhost:3306/pakaiwa" +APP_VERSION=0.1.0 +APP_ENV=production +APP_DEBUG=true +API_PORT=3030 +LOG_LEVEL=info +EXAMPLE_EMAIL=test@pakaiwa.my.id diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2a7164d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +pnpm-lock.yaml +package-lock.json \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..24ee99d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": true, + "tabWidth": 2, + "useTabs": true, + "endOfLine": "lf", + "singleQuote": false, + "trailingComma": "es5", + "singleAttributePerLine": true +} \ No newline at end of file From ae65eaf2a57cb7dbd7425585fe75e54dd2ef3d59 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Thu, 2 Jan 2025 23:02:52 +0700 Subject: [PATCH 04/38] fix: package name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa4378f..5027246 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "BunAPI", + "name": "bunapi", "scripts": { "dev": "bun run --hot src/index.ts" }, From 09ce92072cd0fa650fbc879268ca2b53aff05c4b Mon Sep 17 00:00:00 2001 From: Call Vin Date: Thu, 2 Jan 2025 23:10:21 +0700 Subject: [PATCH 05/38] fix: type to module --- package.json | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 5027246..363cbfb 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { - "name": "bunapi", - "scripts": { - "dev": "bun run --hot src/index.ts" - }, - "dependencies": { - "hono": "^4.6.15" - }, - "devDependencies": { - "@types/bun": "latest" - } -} \ No newline at end of file + "name": "bunapi", + "type": "module", + "scripts": { + "dev": "bun run --hot src/index.ts" + }, + "dependencies": { + "hono": "^4.6.15" + }, + "devDependencies": { + "@types/bun": "latest" + } +} From 2c083268542331e4ed721b36de736f52095a6417 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Thu, 2 Jan 2025 23:14:11 +0700 Subject: [PATCH 06/38] chore: add lint depedency --- .github/workflows/codeql.yml | 74 ++++++++++++------------ .github/workflows/dependency-review.yml | 8 +-- .prettierrc.json | 16 ++--- README.md | 2 + bun.lockb | Bin 3028 -> 47787 bytes eslint.config.js | 27 +++++++++ package.json | 10 +++- src/index.ts | 9 --- src/main.ts | 9 +++ tsconfig.json | 12 ++-- 10 files changed, 101 insertions(+), 66 deletions(-) create mode 100644 eslint.config.js delete mode 100644 src/index.ts create mode 100644 src/main.ts diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5cad2cb..3c71725 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,11 +13,11 @@ name: "CodeQL Advanced" on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] schedule: - - cron: '40 22 * * 5' + - cron: "40 22 * * 5" jobs: analyze: @@ -43,8 +43,8 @@ jobs: fail-fast: false matrix: include: - - language: javascript-typescript - build-mode: none + - language: javascript-typescript + build-mode: none # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -54,39 +54,39 @@ jobs: # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # โ„น๏ธ Command-line programs to run using the OS shell. - # ๐Ÿ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # โ„น๏ธ Command-line programs to run using the OS shell. + # ๐Ÿ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index d19e21b..e71af6d 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -7,10 +7,10 @@ # # Source repository: https://github.com/actions/dependency-review-action # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement -name: 'Dependency review' +name: "Dependency review" on: pull_request: - branches: [ "main" ] + branches: ["main"] # If using a dependency submission action in this workflow this permission will need to be set to: # @@ -27,9 +27,9 @@ jobs: dependency-review: runs-on: ubuntu-latest steps: - - name: 'Checkout repository' + - name: "Checkout repository" uses: actions/checkout@v4 - - name: 'Dependency Review' + - name: "Dependency Review" uses: actions/dependency-review-action@v4 # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. with: diff --git a/.prettierrc.json b/.prettierrc.json index 24ee99d..a120c29 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,9 +1,9 @@ { - "semi": true, - "tabWidth": 2, - "useTabs": true, - "endOfLine": "lf", - "singleQuote": false, - "trailingComma": "es5", - "singleAttributePerLine": true -} \ No newline at end of file + "semi": true, + "tabWidth": 2, + "useTabs": true, + "endOfLine": "lf", + "singleQuote": false, + "trailingComma": "es5", + "singleAttributePerLine": true +} diff --git a/README.md b/README.md index 6dd13e7..d950ba6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ To install dependencies: + ```sh bun install ``` To run: + ```sh bun run dev ``` diff --git a/bun.lockb b/bun.lockb index bd8f250454e0c4e9fd488b7728f3cd493b09a6bc..b36abf446d7dddcbad52fb0d9cf4ff5ce32ecb3c 100755 GIT binary patch literal 47787 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 J(f_{x{vQ@$1F--A delta 618 zcmZ4emFbH31U=1Vv%VYNou0q#yvI7-=rzL8HvNlx)&?*+ED6}+#hx8=)P`z z%Liq%LpTf!PNjK{0iFyD8TonnFflF$hK52QhZ9Ke1JXd94a|%T3_L)3D^#58b(y1O zvbI8buIhfxwO%2UH8=_wxhBu#uxGMnm>i{M$5h2Ic@~g5$uRjAkm6^YETwM86u>w+ zO5JMm1P+!&Ca85F8fZ8JGZ2H=tWZ81kOlz|4Uz`|kUY`)CP(U@nY=Z^n~`}kV`TW` z69&AKQzN|@StoA<@+u5@CyPeaPreYf)R6rH(g{)uQU;$1r){6bU<6 zym02*9D5ZwYd27Vsh*LUo(WI^B<3dSNA;?0wY6ko)B=itKnE~lA@pRCsSaE_pj;d7 k$&03{bCgsTq!#NZl}=8aEi;*8n&ISxjcl7=?>N8+02Pyj;{X5v diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..a1dca93 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,27 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { ignores: ["**/modules/index.js", "**/src-old/*.{js,mjs,cjs,ts}"] }, + { files: ["**/src/*.{js,mjs,cjs,ts}"] }, + { + languageOptions: { globals: globals.node }, + rules: { + "no-unused-vars": "warn", + "@typescript-eslint/no-unused-vars": "warn", + "no-console": [ + "error", + { + allow: ["warn", "error"], + }, + ], + semi: [2, "always"], + indent: ["warn", "tab", { SwitchCase: 1 }], + quotes: ["warn", "double"], + }, + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; diff --git a/package.json b/package.json index 363cbfb..2eed9db 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,18 @@ "name": "bunapi", "type": "module", "scripts": { - "dev": "bun run --hot src/index.ts" + "dev": "bun run --hot src/main.ts", + "fc": "bunx prettier --write . && bunx eslint --fix . && bunx prettier --check . && pnpm eslint ." }, "dependencies": { "hono": "^4.6.15" }, "devDependencies": { - "@types/bun": "latest" + "@eslint/js": "^9.17.0", + "@types/bun": "latest", + "eslint": "^9.17.0", + "globals": "^15.14.0", + "prettier": "^3.4.2", + "typescript-eslint": "^8.19.0" } } diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 3191383..0000000 --- a/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Hono } from 'hono' - -const app = new Hono() - -app.get('/', (c) => { - return c.text('Hello Hono!') -}) - -export default app diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..37ce2cd --- /dev/null +++ b/src/main.ts @@ -0,0 +1,9 @@ +import { Hono } from "hono"; + +const app = new Hono(); + +app.get("/api", (c) => { + return c.text("Hello Hono!"); +}); + +export default app; diff --git a/tsconfig.json b/tsconfig.json index c442b33..9c845ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { - "compilerOptions": { - "strict": true, - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" - } -} \ No newline at end of file + "compilerOptions": { + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + } +} From 8a84cd7a1dddc4be11a900b3bd678a981c2d06ef Mon Sep 17 00:00:00 2001 From: Call Vin Date: Thu, 2 Jan 2025 23:17:04 +0700 Subject: [PATCH 07/38] add: [DB] Prisma --- bun.lockb | Bin 47787 -> 50992 bytes package.json | 5 ++++- .../20250102145130_init/migration.sql | 9 +++++++++ prisma/migrations/migration_lock.toml | 3 +++ prisma/schema/schema.prisma | 9 +++++++++ prisma/schema/user.prisma | 7 +++++++ 6 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20250102145130_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema/schema.prisma create mode 100644 prisma/schema/user.prisma diff --git a/bun.lockb b/bun.lockb index b36abf446d7dddcbad52fb0d9cf4ff5ce32ecb3c..d06a7fc2f4a7c199be62c9ec4ada609db6d86ee1 100755 GIT binary patch delta 10026 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 BnWF#z delta 7932 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 Date: Thu, 2 Jan 2025 23:18:48 +0700 Subject: [PATCH 08/38] add: [config] dotenv depedency --- bun.lockb | Bin 50992 -> 51325 bytes package.json | 1 + 2 files changed, 1 insertion(+) diff --git a/bun.lockb b/bun.lockb index d06a7fc2f4a7c199be62c9ec4ada609db6d86ee1..8be9a86b125179db9cc7adb549129a1831aeface 100755 GIT binary patch delta 8412 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 diff --git a/package.json b/package.json index ad4caa8..4f8f464 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@prisma/client": "^6.1.0", + "dotenv": "^16.4.7", "hono": "^4.6.15" }, "devDependencies": { From 75f0a86716098fcffcc44e36de9cf361a2724764 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Thu, 2 Jan 2025 23:19:18 +0700 Subject: [PATCH 09/38] add: [log] winston depedency --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f8f464..e09704c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "dependencies": { "@prisma/client": "^6.1.0", "dotenv": "^16.4.7", - "hono": "^4.6.15" + "hono": "^4.6.15", + "winston": "^3.17.0" }, "devDependencies": { "@eslint/js": "^9.17.0", From b90d54ffdceca3335aa8fb5b1cc4616f6f043f94 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Thu, 2 Jan 2025 23:19:32 +0700 Subject: [PATCH 10/38] add: [log] winston config --- bun.lockb | Bin 51325 -> 61929 bytes src/config/logger.ts | 69 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/config/logger.ts diff --git a/bun.lockb b/bun.lockb index 8be9a86b125179db9cc7adb549129a1831aeface..7b6225ce2446a6f93ec2aa0a4cb437acbb75eb41 100755 GIT binary patch delta 15295 zcmeHucUV+cxA)AEQ3e=9undSwRX}E>4~UFlMMn@Tb}0@rNFP+N4Op?D=(b?TuBcH! zV~fV#YizMalDrzl^2TV4F_!PQ&df1mZgTJazUR4re9v?D!|$xU_S$=&wbou|pE-wh z-j~*S{9~O`rT6`J;n<#PMk?tiFyelcK$TX16MkR}nO2d`W+416Kt%ewjT zEeuQ67#WeM!|2SYxNaiRSY4(*FDp}Y9)boUQQ;9#YtRLt^+ETEMIu|!ttQ%&Gs<DpY>wV8=Y>Dm#ZjS`W_3hjB?{5(afHa|Hl zJ&WY$>WV}*Xm_n=R9Fv6=V<~;{ST2`X4Wvj*u`)aXC$FkpRUWy6N%np1o1;LlO1Tf zc1$Mb7WDy7?U5$h9h8(Eq19$9bb8Sfj3aOT07~sKa2fHHpfqkBDEK#rk>m7ty? zk)cqhSBy;0N)ox_Y}9ch1o$h|jm*r-)rv&RO?-+jbA)1awrCJsLh_y_nypCBN>0p! z7U{YSU7n~Xd`R3ku=rnN9XC%^&+i4 zXS6oAKs2MFaiCV8ovVc+I`qSsLJo_}FH>?>ZYCxgigvQdyRoso8+Z(bXOc#bgj@|C ze}%qHjOQr=rB%pI%u7`;1=rMYOm6f|YNB2NHAn6RPa|l-sApmtud&F^NhHEw;X6m; z8Aj*o6lsXI?9rJ8$yp<`z8C~+3tNEFIBQVS91ltzg559rYS=we+71<8;^P&lhF8CsCyJ;0Mf*||D>26l%O zy>^U2o0+Gt*V;JYFQ5pJ!tJ2svN@pS+NCBQv6z>qgSI9Y!%E&#rP4U@cu;HfKMYD9 z$~CoHJsA@fmfYHBx z-tfEXz&8(*HtToIaJ|!JSgT>KKQ&lZ+eCi<+Q@}Fs_n-Y2PD0H`XTf6)+1-u?>c^c z_sx0HgT7fZ-kK48Fl3N*U-?zv*(|`Keuc{vU{*sl^ z|KYypH)6YNdNS|(pvHSsdT)2zdaI@T!5bHxcWr)qG(EE;D*nNw)2mtXXeyW>aOohQk|&(TI8(M92zcQfW za<^BjA0*{?L_}KG4;A?8y%vRH7Fl;db$Q)6oJ0lZ)L}VRD%m}_usb*lP${dd-G~KLME>x67@E--(IrV}?PTm@tCH?C^?@9-{Q!=)kmuxep$$K`zl}=T7u=ucodb?6s>A!~ zCb&pr&*myw8{Des45Tpt4ztkhh96-xuLuwfM-88)mIWVa3Tu~u^rY*-qI ztBh5E43)83kR>waW22T{z_rh~UY}JU zX2xUJ!2UpCg+O{%pZVCTIfWfdvsFuU*m2ZMVH3`6vSYQjYUv$nKeLdewp>pIVSs z(w4Z*(TVDizqug}tfGNhx&)0hybiDUbqD5Sua-8&7G+#@fAs9tkX6{Lx%mxQEo2uU z3&sE|tcr@WZp6|Y)RKX?dA4QGY{R)P8?jmkwe&F>BGJndtF4j-G&V*dqTOC4Ne36l zw$~4reuY{9dRZ{IS^69tsZx)vJ78)rde z!OFg$IO;jw#96RPMC=uC6o>UlD@h~Vib8okVo;+q^ZIeK9a(LY_F_ktCRa;+aAzI@ z5!8^XB-6pgF@<%w^ag4{{1EIToK;hn=BVa6G-VZzYH4v(ktiH8F&QE`1}=&pW8o|k z_2b(qNCrBy3MVyJ?#ybP)Y99KML{NFIgTo>X)~7ARLugt>6$OK`D9 z4#xLvZd@uDh~=LKE)+8Ua+e+iXBWbe6?IDIjt}1B)IFrr}DsG1ttMyQGzj`qr zPc`S|&C)?7Dd8#U4gwp*(tSHT6b?b2|rackz&N-c@S z?*lb8cm?6+F%BK;ibNvX6@?;2J31A%*|p_)uJu$kvy;jtU!XCXhDlzd)|RjNDNJo# zv4T~2silX}K*3U5e4PX{=5!ncJ16;T!) zP>7N=n-~jPBgZQh|+|l z6-`hGP$>k6FEY`|pccS%fQmV#elz&i{|@bFJmE~!0CP$c%`&wUl>*Gf6D9dV;#rKs z=*K0*p%5i$89??g2dJ1+GIJ&0YNR%(ZvpB7I{`Y|K7jfi0H_e9dA~BzgC=^2JVpU` z44?rg04hXjz)2H5Wum7+shCshcLt#G=S=hhC>5e4zeo%UQQ|KVgK~*qF}`A);0o$> zftx0J50nZ~I>CKnP>9k({zwc8QJVN)#Gnu*5B@|9iaDizj{uslR*0A)hZK8kKqzs~e-ldE){Euu>m^zW4 zo(=PD{J*=E(bI(>wB$u5IvLagFyE!X@;vpMK@7@&O*(z_;IUB^w zl=75EP)jWOQ2zrcZA(J2&@Ha%-*VKHzOwAB*>PVUSk+kP8PZ#s^>A3u&6PWw&P^`R zwcK@X_avvIf4u!;+OoR0qoY?`ox1H%yD2a9ebTOVn7B;Lng=y8y#DvoRW4hTvwt6W z$D-4OQw~L6CV1ACXLjx}+RajF@4El((4)5`C1GcFbn*#l&aUn|UgI|>?a;L)T`VjP zbiB5r+)O>X;R(gU?&sgP4qkG4NVoMq=T@`|UQtl{U7cl*LZa3jwk$4TI+wy{aWk)X z=;-LbnL9tTNyrKLgovT3J3cD>?Ow`@Gb^9z*@YmvVS1Lk?|$he(c1|1%LS26%Q9|mYJEL5;nCWrS+hL)tn*n?9y{;J7n|N$<(p|& zPf!ii;)|8vtcyLHb8Dk_<&L6tIZeaDHZV+c#pl}aEgN>F{<~*cLHP?uX4`S> zv7V~$r##;KQ&PsFy?;y|mX?|H`{bc)VzAu6{y3?d)IO>Ip26+rSiQO&Hm14D_}b8| zBR4n6yx{-%;zGys)*fD`2Y$O_>I(USz`G~APnP7Ljc?s}@8}szU9DCfG1E?Ju3eA* zq94L8J^gj?vbg9;j=je}n04~f&;|X@_lR0y;nqgkpndGbQ;&4Pj;m%p-_Y;C^3qLX zr=QC`*sf0N!DoiQn78NSe5MGI_sAOectgO(U)#^Cj4QHx(BQ{4i{1C!YkMK6@5a%8 z+zf7zaNFkGk5L!5_pzRKde*4IREM0jM$O!|_Bry~_4$WyJ!{}>rkyog(=nbs3XvH? z-xv3aol!7(=Is1kmkNVlN1g95`FeHH-8X@?TkS(;{CsId-op=Xo=#BCyK>0g^8D`a z^Q(QGcC5Un?(N<2-v`6#)t(T|GINt3r<9F5{(Yyz8*YBP_4LX5|9&?nYyh{rs?nsl zlZ_W`%KCQekdkX9MWK#$!pio0S|!IxtFEk_J6#rH@JOW*tTWu z#VX%68`tg0)89SdQLlQff3MobH&Ut=-*_L888f&l^z_#Ll0>zf}CvUvrb zA5Q0-Jb%-WXQo|!bM3s+Z%@1UcwdM0m)sR^W)+TG&HCOr+x~HgGTn9G!S~DKd}F+w zY@g&f{l(iq4(0pFZE!Y;4W?-V=uU+gAPd>e$GwjZaB-H_Q!*o)+5l zwwZQ#+4QMcI66M8S62V@vdC#c&AKi8X49Qz5v?Cw{bqCGF}p7hPuy;0yF5H1Z_;YV zi9b2FcsMEd!@MU)oRzhzKjjisS1ucRw`2JNN}a%<`F;7P$xZ#4?%gpBc$1wMJ! zn_B+*(}Ee(YJ!&-G7nunxH8FW)1a~YZqLixCo}1IjOgp^x`cy22?HJjj zYQw;#OSs$3hWK4^IPmmQaBxv@y9 zpLZX;z2>)bb|-JIt6?#1hOYy8q(4A=|S@*7Ko8VFp2X64Z;&8)W$Q%;29!&p z#*{{1kuG|)WSd)Y%C`@FG#{q*JenbHbH{T1(Ll$*qgKz~DUVMM4AVEd+hgCcCoiUF zwC?)q!b|oZ+UHhXW43 zJgUjbOk9{6G6a=-UWw%VO*{9#3PW#i+Ems`K{>o<7+ z9tY>_{ikAvlsp)=d`*L;fz@XV?9H@mYVJFa`XA?8I_^GD_Ojs6iT2xvEN$6xY+Zl< zK5yET-)=Vl_0tgvvyR{KJ?K~^U7@twbZphffi55Ho1OWsvZtN@$c%zxz1h%q@*V|! z)2x2gY7gwZn|A)mxHiG=k@>RjH@7%-{pCvX$1%rxyy&?4*W`9h7j=I7Q|jJfmwFu* zuN!=G;<*j)ez|X($_*cArd>03q-#8rN5~9OKR4Wce@COsibnOgpIxew?hQ{&@4eY_ z)~(}Hp7gFJla+%IZ&c(CTqlN+Nl-u{y87;($j#i4ImdE*!U zeQkc}{=l>0JCa6BuHl(A>vyjfUCJPVDIv6UejaclM%+#_%?Dvemdma5`eSqB+=53UXC z78=i@JIdJhP>tB1{SNL8xS?ShaUk0g7SA?!k}+9ZjX0PMY#Yx8be6G$;8aW+9?vYh z$XG_WMjXcWf!hnNSv!q5oax%dv(&CKb^%-jbBut$qh)Mjght$+odI_moImz;$Ha?O|Vc*wnUT}&Khw)RtauE zoQyTx~oP!m^ntnzTU7XS|c9H&VZBmfkoXk;^C~I z8|(vj4_qSi?hgCT=YP!SWk^Ohi&Nz`@qTKG-5p)7zg_X zVa33WX3}1;Z!lJ@mqt96?E_~y1S{5CBQ9XN-mnkc1#sh;V;|Uu|MN`jqY+PJXTZsa z!M?s4v4Iuzg?-@efh%I({b1j4*w;@Zp2BW{^Gbky{WaogY-ay>@pSe*jx$(Te7v}r z&Bt*jdyL~O7BwJVJew`WaSnTdV+rdvFkU>Dt-)~~`yIzp)@x9_cs|>L;{qlg94}^U zAdU-JC60@jbV$5-F-yd83EPKb8EY^!Uc8j)a9qZY;JBPQ4vQDBU^zIhWM^<(#axHS zi&wJ(9Lw1i9M>@Kgn02D@v7kg3iKi9*>CAeK*n=iN4*y@Yuu3##J8W;n zVn=MGCYhz&_^)SsP&-p^3o(h<(-ccdRCD}~-PG*NPr@$iEG5J6%7W(Z!~D{0xD!51 zoidOG^LZ4o!3Ov0K z2?MCm%QfmlPc7{LDk}hLqo?of02O-aMs4)?Pz_L71yCEkvg)u42O6*%pho&7-wL2o z4p5sV0P}d+8dKZhwWy#(Z(t{irxm1KYdq!OU@ z_X3=O#Xu2|3Fv@Bz}LVL;3(h&qycAvGr%$6IN%GM118rmIEcg3|Aa> z0f&LF0B=AEH-p=9zb)*0K|=f4;&gy9UB1jTaw5rYLPihd0y=;U9SMv8k^x$rL|UZ;R0abS;c)<62C+a7pc@bk zbOAa8oq+a07!V53YWK$Z`AQp5;c^N_ zohA$i+5+u>2tW-)0v&)Tpd-)~&;Z>5QkLp50JRgWfxbW=pcl{^=m!i0;sFC)F}PZI z*A4?E3+YN40t^RI04+dTlb6UlO0c3R=kPc)4nE+j}2|yk|5I zE~NFJhzhxj){)k10Wcnz4+!f?>q`?%00h}MGdamh0rFZQFbSY}h5jC>lXr-xI(ePE zJ)3GXQ7Hyy07bwQU@|Zjm-ZB1 znZ7~_9Iv+@xlYIl7Lw(7<^0JaA!}Gjr{g7lBoXq5g~U2eJSkX-<@@X689r|3#HPjj zlte6K*YVQ@lHNj^v5L8J@$$FFX?H(Imh$M*ctAd|D1o7vVZdtLyy+?uTi*=Wta1*x=VHl=cBAs;>S_pmoHX}z2BC*(JT zAFNp&GJeExJVne8Kk=nyIU&f`q;47r2!#A`h5Pr zUsaN}-AEaVeEhVCv@u>R)n^O~I*i=r$>G6WhrmFG zcaF?uk({%0Vr>@%aMso=6C879#fx@v*P1c);sDMV1zb%tHV54M=Irodd5Dnr{`3vr znmJeo@jCHt6f)qS*tj@aPxcIegj@z~jj@jBVCJ>t)3or?m?b{y5?B6b@TYGcYI~nN z^R0Gc8t)o@a}n~!j}P?w&1%k<^B^%rVLvza-4c1oKZof*?>y72m~WZfInm9SqdtGn zv12+n|KdM4ph<2lp-j%LbYp%?ntSp`?p{3IblizC*H4op#5OIV40jVdg1ePIREp8X^c)&_rK|< zjxg?@F}Cb7W;b14!bbqJM=Y%T>h_uYP=X>2(LTbHHD4~L9VT>n0GH><(n0onvhwBf z5aFAHh?j>e&)Nm2Vw4I2jPMh_La-PR(Dy>!iW86o8zc8jOZI$u5AI+q)^3H|S@`H+ zMm_IUG3%-}p^!tf;E$+F?oaxXS(MZo9Qv!7QuncU+1&mmy+zsG;(f8#C2xO&ci zd#B-dLt~Jb7WBU{VLjw6t_tAhv}QG{6|#kc#(^Q4a?YKF(CXTe`9|Q2w1Wjt1JLLam2BnfISIa|$uP4@>D=S!?_Kgm| z)k2lQN`F6*@SVl{HiJr9?v4n9BmkREfHL&3l1*4G_xO(;==qRJIsM2&sX4;-u8yp0 zWZ0Y4ey#4|evaEi;ndLJ^WuEl4|Ye5!d@?o)T-U+P_>IcOF-svYI&^igwRIO#`LV9 zz-ED;=gPd1<03~SCZ)C+p-aSfRQfz!vObdkVWn?lB>3z~pO>pm%mC0cU29_Xi78YS zT6CEs@mUM_Nf}P<-6Rw{zW_V`AZ8d#@(6>hB?r7_y{e13YAOWaw$e=@wp6r97c+{ ze%^$sK5xJoSMGEV{Cpbp`n*BGKNIlhBTprbPD#-+*UDzjpSWBa^Czdk&5hMmyE+O_ z5fBSi1#KTeY|YLV_=t`FiV#9m4KiUR^9r)HdUNNHIsSjUQo_0ax`LQ3m|fZF%9J~N z>+_Ec=*d$>5SxF*of*EW+uZyV5P!BB%0O1S&&}zx7(yQ-52u9C>@Ca!1wu1U!T%6p zQ5#$u2~UpbAXMS{!&}@POivgPm}+Qu-Q(`~DOMr*R43n&UmNfbwvg}?;Sj(Qcew@p zHTu!P>{{R*A4sccG$0$1&5E`<+tR0&=*mBxC<1t`OwTZAHPwvmEUro-*V8~q{+tD_ zz>S%fw2|<%0=ZB%8cIW33!_M53=vdOBxhyf3$i?ZM!F^u)3n?eQ&(kVKaDi9IAfUk z1oSPbUY9{vseY_(+_-c;`U5|2!W^GBV1e*_Lu$aoiMhFn1v))*J>=Z%Pc{{U%s7k) zWUCQUaaHX|J7!fSuLE6J_BLvBW;xqjs=qAHx2+zuV=~8X?sR67s+R0wb<^rORWBO- EA2c2FXaE2J delta 9246 zcmeHMeOOf0y5D;b18iUbQE|XgKoAjSf)N=$WEA`e+=@UV;3r=rf_%w<0;z!M$+B+h zTjy$Jigik@Ybu&TVv%;!K5pLoWGBnIbvoUfb+pqtooU(-n+=<-V>3&-Uwd`Kx&B`tc;IZ^sql!!J;Fyhaqpaa z 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 { + return `${timestamp}|[${level.toUpperCase()}]|${message}|`; +}); + +const logLevel: string = process.env.LOG_LEVEL ?? "warn"; +console.error(logLevel); +const todayDate: string = new Date().toISOString().slice(0, 10); +const logFolder: string = `./logs/${todayDate.replace(/-/g, "")}/${todayDate}`; + +const myTransports: ( + | transports.ConsoleTransportInstance + | transports.FileTransportInstance +)[] = [ + new transports.File({ + filename: `${logFolder}-error.log`, + level: "error", + }), + new transports.File({ + filename: `${logFolder}-warn.log`, + level: "warn", + }), + new transports.File({ + filename: `${logFolder}-info.log`, + level: "info", + }), + new transports.File({ + filename: `${logFolder}-query.log`, + level: "verbose", + }), +]; + +/* istanbul ignore next */ +if (logLevel == "warn") { + myTransports.push( + new transports.Console({ + level: "warn", + }) + ); + /* istanbul ignore next */ +} else if (logLevel == "info") { + myTransports.push( + new transports.Console({ + level: "info", + }) + ); +} else { + myTransports.push( + new transports.Console({ + level: "error", + }) + ); +} + +export const logger = createLogger({ + format: combine( + timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), + logsFormat, + colorize() + ), + transports: myTransports, + rejectionHandlers: [ + new transports.File({ filename: `${logFolder}-rejections.log` }), + ], +}); From a028287d3049448db8a6555d5c9feb27ba710c6a Mon Sep 17 00:00:00 2001 From: Call Vin Date: Thu, 2 Jan 2025 23:20:51 +0700 Subject: [PATCH 11/38] add: [DB] Prisma log config --- src/config/database.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/config/database.ts diff --git a/src/config/database.ts b/src/config/database.ts new file mode 100644 index 0000000..a5245b9 --- /dev/null +++ b/src/config/database.ts @@ -0,0 +1,40 @@ +import { logger } from "./logger"; +import { Prisma, PrismaClient } from "@prisma/client"; + +export const prismaClient = new PrismaClient({ + log: [ + { + emit: "event", + level: "query", + }, + { + emit: "event", + level: "error", + }, + { + emit: "event", + level: "info", + }, + { + emit: "event", + level: "warn", + }, + ], +}); +/* istanbul ignore next */ +prismaClient.$on("error", (event: Prisma.LogEvent): void => { + logger.error(event); +}); +/* istanbul ignore next */ +prismaClient.$on("warn", (e: Prisma.LogEvent): void => { + logger.warn(e); +}); + +prismaClient.$on("info", (e: Prisma.LogEvent): void => { + logger.info(e); +}); +prismaClient.$on("query", (e: Prisma.QueryEvent): void => { + logger.verbose(`Query: ${e.query}`); + logger.verbose(`Params: ${e.params}`); + logger.verbose(`Duration: ${e.duration} ms`); +}); From 37f2267cb9e8230d8242f192934c9e7605d8075b Mon Sep 17 00:00:00 2001 From: Call Vin Date: Fri, 3 Jan 2025 00:02:20 +0700 Subject: [PATCH 12/38] add: zod --- bun.lockb | Bin 61929 -> 62256 bytes package.json | 3 ++- src/main.ts | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bun.lockb b/bun.lockb index 7b6225ce2446a6f93ec2aa0a4cb437acbb75eb41..6aff71b3578fa65b5f6ea8c74735832dc9da2090 100755 GIT binary patch delta 10279 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 U5je*3B?gJN!}rfSdC!Rd2GpE!`2YX_ delta 10053 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 diff --git a/package.json b/package.json index e09704c..09db694 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "@prisma/client": "^6.1.0", "dotenv": "^16.4.7", "hono": "^4.6.15", - "winston": "^3.17.0" + "winston": "^3.17.0", + "zod": "^3.24.1" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/src/main.ts b/src/main.ts index 37ce2cd..9118de6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,6 @@ import { Hono } from "hono"; +import { HTTPException } from "hono/http-exception"; +import { ZodError } from "zod"; const app = new Hono(); @@ -6,4 +8,23 @@ app.get("/api", (c) => { return c.text("Hello Hono!"); }); +app.onError(async (err, c) => { + if (err instanceof HTTPException) { + c.status(err.status); + return c.json({ + errors: err.message, + }); + } else if (err instanceof ZodError) { + c.status(400); + return c.json({ + errors: err.message, + }); + } else { + c.status(500); + return c.json({ + errors: err.message, + }); + } +}); + export default app; From 46a335763992ba355cc3fdbddb47ab9e670c074d Mon Sep 17 00:00:00 2001 From: Call Vin Date: Fri, 3 Jan 2025 00:26:10 +0700 Subject: [PATCH 13/38] add: [IDE] config --- .idea/BunAPI.iml | 12 ++++ .idea/bun.xml | 6 ++ .idea/codeStyles/Project.xml | 62 ++++++++++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 ++ .idea/inspectionProfiles/Project_Default.xml | 6 ++ .idea/jsLinters/eslint.xml | 6 ++ .idea/modules.xml | 8 +++ .idea/prettier.xml | 7 +++ .idea/vcs.xml | 6 ++ 9 files changed, 118 insertions(+) create mode 100644 .idea/BunAPI.iml create mode 100644 .idea/bun.xml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/jsLinters/eslint.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/prettier.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/BunAPI.iml b/.idea/BunAPI.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/BunAPI.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/bun.xml b/.idea/bun.xml new file mode 100644 index 0000000..56b40f0 --- /dev/null +++ b/.idea/bun.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..32d1432 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,62 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/jsLinters/eslint.xml b/.idea/jsLinters/eslint.xml new file mode 100644 index 0000000..541945b --- /dev/null +++ b/.idea/jsLinters/eslint.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..08ab724 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..0c83ac4 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From e059f0db4954452affc0feb263b7519ef79e9963 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Fri, 3 Jan 2025 00:26:27 +0700 Subject: [PATCH 14/38] add: tsconfig --- tsconfig.json | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 9c845ae..c0ea41c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,28 @@ { "compilerOptions": { - "strict": true, + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" + "allowJs": true, + "jsxImportSource": "hono/jsx", + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false } } From 9b7156b14995d6c41cc3de5ee5aaca6548f86961 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Fri, 3 Jan 2025 00:33:15 +0700 Subject: [PATCH 15/38] add: tsconfig --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 9118de6..6ccd8e0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; -import { HTTPException } from "hono/http-exception"; import { ZodError } from "zod"; +import { HTTPException } from "hono/http-exception"; const app = new Hono(); From 37b6f13439b756ea31567a4d8c22d206120cd17b Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sat, 4 Jan 2025 17:37:39 +0700 Subject: [PATCH 16/38] fix: Hono main Config --- src/config/logger.ts | 1 - src/main.ts | 63 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/config/logger.ts b/src/config/logger.ts index b41be2e..9e4ae90 100644 --- a/src/config/logger.ts +++ b/src/config/logger.ts @@ -8,7 +8,6 @@ const logsFormat = printf(({ level, message, timestamp }) => { }); const logLevel: string = process.env.LOG_LEVEL ?? "warn"; -console.error(logLevel); const todayDate: string = new Date().toISOString().slice(0, 10); const logFolder: string = `./logs/${todayDate.replace(/-/g, "")}/${todayDate}`; diff --git a/src/main.ts b/src/main.ts index 6ccd8e0..02eb76e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,35 @@ -import { Hono } from "hono"; +import { Hono, type Env, type ExecutionContext } from "hono"; import { ZodError } from "zod"; +import { config } from "dotenv"; import { HTTPException } from "hono/http-exception"; +import { logger } from "./config/logger"; +import { serve, type ServerType } from "@hono/node-server"; +import { prismaClient } from "./config/database"; +import { error } from "winston"; -const app = new Hono(); +config(); -app.get("/api", (c) => { - return c.text("Hello Hono!"); +const port: number = Number(process.env.API_PORT ?? 3030); +const version: string = process.env.APP_VERSION ?? "0.0.1-alpha"; + +const app = new Hono().basePath("/api"); + +app.get("/", (c) => { + return c.json( + { + message: version, + }, + 404 + ); +}); + +app.notFound((c) => { + return c.json( + { + errors: "Not Found", + }, + 404 + ); }); app.onError(async (err, c) => { @@ -27,4 +51,33 @@ app.onError(async (err, c) => { } }); -export default app; +const server: ServerType = serve({ + port: port, + fetch: app.fetch, +}); + +process.on("SIGINT", gracefulShutdown); +process.on("SIGTERM", gracefulShutdown); +process.on("unhandledRejection", gracefulShutdown); + +async function gracefulShutdown(): Promise { + logger.info("SIGTERM/SIGINT signal received: closing HTTP server"); + logger.info("Shutting down gracefully..."); + await prismaClient.$disconnect(); + + server.close(() => { + logger.info("HTTP server closed"); + // Close any other connections or resources here + process.exit(0); + }); + + // Force close the server after 5 seconds + setTimeout(() => { + console.error( + "Could not close connections in time, forcefully shutting down" + ); + process.exit(1); + }, 5000); +} + +export default server; From b594be48a410ef13ffe064dbb202fc461002d5c1 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sat, 4 Jan 2025 18:09:15 +0700 Subject: [PATCH 17/38] add: app version on /api --- .vscode/settings.json | 1 + bun.lockb | Bin 62256 -> 62662 bytes package.json | 3 +++ src/main.ts | 17 +++++++++++------ 4 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} diff --git a/bun.lockb b/bun.lockb index 6aff71b3578fa65b5f6ea8c74735832dc9da2090..fa068e0de447a2a491ca6d535b9d76147e8528c2 100755 GIT binary patch delta 10313 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 WlKy~0!`|&q)6Rr6KlH}qz5fS8^CCY0 delta 10145 zcmeHNd3Y67makjM1B$F5NGb~uNC*KENM4ec5RxaWV3Dv$$N~Y9@Cexxk|2>KkF5m( z5f9n~L=B(}t3U#TO(0QOTyX4LEJl^f%u(GyizM_p5V% z_uTEAd+Juzt9t#q+rB@$JsBB(ydwUf@?QH*!{6(@W%#lITPz17zW>v}zpfY-TCx8R zx5V)mzL4~^ZC!c~dHt4k^LmDVgMv2BfgK-m?A zC6&k^&2r?;&Vh_|lcdfl--$do`hj>_VK(ke*` zM_0^OL;6A%JLZ<6so?!hVQg$qMHm4LmS&ecwCF6dhd>%NO!Y8@a`Oueu z~_iSFG=`0`{TfUSI;Sk zC`1&MSC`i0mCbZaK_iZ!R7hUs!H`@p5VAX@k6GRv_!GUq^E+6`4S5hY_9hOxzo^Zv zTdf<~vS|rD3^@RjyFUZTF5o50fy>&^itD`v$-_4HGy=U5Tm^pylBeNvVGA0gigPC< z>u2_sq!7qGy(9@;JI8{<6SdA|DBzBN9#4Np1)jiG#18s)UWa6!J0~a40l&=XW0b!H ziFk9~hGcz-V@|#!`p0l0v}1E`gc)nQ)Z`=j^0@Aj^Fm+xhkJMRP0WKQ{3K)$z4IUs#SX}1^;OvHDknFCr{i)E>UCoX(Cb1I|>*2fw$;Q<;)LX(t z!#nLpy9+C+9(IvSnK*h<9gdT!7030YdfMf`3(E3L7K5qI(=Jw0D~@MK^|H%9$du)k zEFPpfFT1FtRvcd=)!QzAkCb_zUGab`S<{Ykd~6s&?P3>x zl-b3uGik6_GKFHYEp)t6*Av;yZwh*9lwt8CbNT>S|Mha2E0G+EXxC zAsEljh{zLQ!@%5hOMd`k55W#Un=%3CB=;igeqMTq@dyV^=1y(*+Z5h*MxbVUePScP zxEEs$7J%{Sn5L)AaujSBZT3l&zg5YIvPVFTjT0X=M$Rq%>WJ)eQ(JePu*)Q>?_{&oKsAOoha}4H^`xx6$s(BQ`r4Jk zU`d*YO1eMf_kzhkJlPW3OOhs2P?toDJ9hEWdTs@B)AZae;@ zwe_WPBdK+eU0H}So}pf*=HWW@C^Jct1mH^XR1p=XT!EIi3caop z8e#Npu<2lTbn@W{%8E`_ZlREu+J}O>+e8?(M%%?KQmuCRU?gQ(ljUzBX`_`dGOgCc zT5QdHRnrqoTgjIDMqvH*^iN1!GmyBVppuv#V|W-W zf}ZY{GD_f9J+WjC$LJE*WqmpWAvx)iZpQSW0fT*Oaed^oN>3~iuu>+#6P*rlV#z(` z0Bl$;!0Fc{*UJOAode)>4_SLh!3}2V?B0^~`2crVVAf;F<%Om!GG#F&Czh-)0k~Zm zz=6|MfxPU932e^YbP5Cw?Czh~gRA+Z2)y8=Jw$B-BSYWz_C9mCY z``q388#{+Nd*<$KjLUz{f7|B_Kk>H4>HmM9%fKxDSN6Gv%;W{06rpJ%lxnnenx}cu z6|gW0ADd2n$9mD4v6={{RPb*W>X+??`-2{uEgw%A3PxYeBshSu_*TJrV zrHs==6xEGOrw!x0=sPegjTxU#qsM#Ep7EN9rLVzmgJn<9gpGDgNT+|9;6+{&H9S6L zPE4naiC%O9ERmFl)2YM5UR3h1CX(m~SQA*#Bu&_RXK?+ZYed(|-T@z!d73>mNQidipTA2a+GGHHA3MFL1 zzD(GcsflrP9qbxdN|q)jP+b=6%YuDi57U?_ux|?No1%#{`WoyuSoTy+q|=V6ux~2t z%hp6DWoE;^Y}f}jg_LQqZyM~IripAi0@ef;G+o2rGX>LO-*ngqmP6_c*f#_A&Co<1 zodY`y7LlU~2i4@jz8u&GmQUfiurC+(%i(soeS&c!n(Pd*xGPzuGNiv zAMGh7HAFr7m2CN-D{kAUZQ)k(^>9~ge#ZZpLmRFvd_!2uyW{W7hPH*rM8gBi{DfYm z(or&(-v_LrHB0g=U-m-{G@q~Q@W*vkRe@v9_J)N^KkCu((iVLulU8q~OM%*XfVKuZ#}sV-%BL{oHheo#{1G4HsIfYmw;`+ivTDh2ofk8hIl*qoWj z@Jby3cp<`pAb=lEmH_3lW zW-qaK*drX_e8!bxd_C0J>LP%nsvIZ-_*7u`Rs-CS>+q?<%fyYJ1{MMAEnY@mGCu7V z08as~<>ck%_6q@*ZoaA4Uu3e9&slccVt^;b9lIKEOt5#DbKVKCyH^8`11kZJ#3jHo zU@5R1_#N;V!2PZTRsm}OZpYF4BoE~pgeSp+vL&wB@nEdyfMkpAF5dvor`IOnSzsej zXL2rc@fVQa3e*EzOqsD08KlByO5XJ`4cWa2m!o65Rk4#U?zW01Qud?-oJIv782iRe zOUBSXC*x!>%^DdKIS3EhRIx`DpU|p3s{FY`-h2JzZzMXkCpz$+ZQ>%~EWKW|xJ*O$ zs^S_=*{cRDl8x6`!S4^87B(&TFS1w^8yU?5?4-4*6-ztzrU-p7a~SIPvcI@UBVS$| z>w3YNTM?4^?q?ytNlP)MEobRh>wg);J#WEdUIdDjE@xK zox1D&_w%970sWUda(%^@(0Idru2+|p$J2K0m3{sl!^Id@x?XO!t$4p`f9m-Tx+W?* zGD>=@Bc<;53vBDCUuB$XyY!HNzH`^>Gu21G*^!p)j}CCXe61W?ls04dlI5l;aDoqA z*ssRA-rf$5(kA&Y{%|++QIUf%faH3=`{m`{UF&yzWY=59uqKs)4yd@B^g5=}_yaMx zvs-?^8tZx|{Y3qNwC@&PNJD*u0z1O>_IguY$SWzd{Yz-#4f{6G;#X8^*N__MdPAxA1Wg%bnxG4){T zL={KX0N1wyU48c-dvtB-2I!-V01cDMq3H`0n8teKRVcwCy*(`&{ z@H`N2k=Qa_-y<}ft9@!%{_aq{FJ20131uHt1AbY*fYu#U<#)Yl_d&k^*Jldn)-3Su zCSRN<3;2k8ktWiI2dx3FuMH+ocq28(xA6>yL1eNUS5m~G6tRfrA5!uD@aaP-fv(RB zO2WJ6?p&WcP8L?PuR~qv`$N%qE{Zy=;%<_?Jo9j8rMau|jz5EH537N$uMdin)~~4e z!v{Q+d3K?BChz6e67k;#?DLUXD=Nr2Y zrj4S1ZL$XZ!V*K@pL!fo@radh#ELh4B}Y`bIgnNyX%udhbkr*JwS>v5kRL!NkE*e* zj|Fbr&b>1Iz78?^Ty!V7J{$PVv#08@Bs3Zt_6cUG$E)1jllmTuHoM$F)yMiL9S_!T zZSH&r^6A9a&s}o7RH(ZHL5NMYm+@Bp)yzR(bzFUDEi`5rEu$;Pel5)OJwk5mNCS@h z1-jnsuRGk*QqU`E#gALXo?bNLxS#xHFB){hk1|eoj{EO-jh}BkzT!qNx_rE+7)vKk zsq#;{6f?RPezNaRCLlRUk{}h zPWr{oz(pLppfl+|PVGPM8#^0qY=}!No9oLA_l)RCZ?)U`8Z@y+K;G^{e?2)?z7a-4 zPWc5GUt&nk$J&QKm$LQ1cGN@rNZfxdr82k@A80&&D*7j8)90uBO>gK+Y&f|0uQ}M* z>-EJ6yyuRuUpS2r_@}#zqwr>{e5^lRZT1^~7B_lb;0f>A!+y*9)fCGj&Nx9O*ViJo zvtI1ovgYq@KWc&p7~hj5>>WPbS6+?K{v5r)dHAw~J2m@`{h#!ZO%W|>Ts>~SaG`T< zePct-^M&sg;2Itu853> z*^sxJsj1}wcP4bZWl)%>c|BYfAN!`5=ly-(7Ng#VFAUjnNOgR2&nOzdOQqSb_Gx(j N)gekl&$nOd^PdUG%oG3s diff --git a/package.json b/package.json index 09db694..b6a9078 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,7 @@ { "name": "bunapi", + "description": "Restful API by IDScript", + "version": "0.1.0", "type": "module", "scripts": { "dev": "bun run --hot src/main.ts", @@ -7,6 +9,7 @@ "mig": "rm -rf prisma/migrations && bunx prisma generate && bunx prisma migrate dev --name init" }, "dependencies": { + "@hono/node-server": "^1.13.7", "@prisma/client": "^6.1.0", "dotenv": "^16.4.7", "hono": "^4.6.15", diff --git a/src/main.ts b/src/main.ts index 02eb76e..adc627d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,23 +1,28 @@ -import { Hono, type Env, type ExecutionContext } from "hono"; +import { Hono } from "hono"; import { ZodError } from "zod"; import { config } from "dotenv"; -import { HTTPException } from "hono/http-exception"; +import pkg from "../package.json"; import { logger } from "./config/logger"; -import { serve, type ServerType } from "@hono/node-server"; import { prismaClient } from "./config/database"; -import { error } from "winston"; +import { HTTPException } from "hono/http-exception"; +import { serve, type ServerType } from "@hono/node-server"; config(); const port: number = Number(process.env.API_PORT ?? 3030); -const version: string = process.env.APP_VERSION ?? "0.0.1-alpha"; const app = new Hono().basePath("/api"); app.get("/", (c) => { + const msg: string = pkg.description ?? "KAnggara Web APP"; + const stab: string = process.env.APP_STAB ?? "Developer-Preview"; + const version: string = process.env.APP_VERSION ?? pkg.version ?? "0.0.1"; + return c.json( { - message: version, + message: msg, + version: version, + stability: stab, }, 404 ); From 6a5ea84b609a43216d94f47359f3f96b49f5f847 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sat, 4 Jan 2025 21:27:05 +0700 Subject: [PATCH 18/38] fix: Shutting down gracefully event --- src/main.ts | 57 ++++++++++++++++++++++------------------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/src/main.ts b/src/main.ts index adc627d..fd2bf0f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,10 +2,11 @@ import { Hono } from "hono"; import { ZodError } from "zod"; import { config } from "dotenv"; import pkg from "../package.json"; -import { logger } from "./config/logger"; +import { log } from "./config/logger"; import { prismaClient } from "./config/database"; import { HTTPException } from "hono/http-exception"; import { serve, type ServerType } from "@hono/node-server"; +import { userController } from "./controller/user-controller"; config(); @@ -14,45 +15,29 @@ const port: number = Number(process.env.API_PORT ?? 3030); const app = new Hono().basePath("/api"); app.get("/", (c) => { - const msg: string = pkg.description ?? "KAnggara Web APP"; - const stab: string = process.env.APP_STAB ?? "Developer-Preview"; - const version: string = process.env.APP_VERSION ?? pkg.version ?? "0.0.1"; - return c.json( { - message: msg, - version: version, - stability: stab, + message: pkg.description ?? "KAnggara Web APP", + version: process.env.APP_VERSION ?? pkg.version ?? "0.0.1", + stability: process.env.APP_STAB ?? "Developer-Preview", }, - 404 + 200 ); }); +app.route("/", userController); + app.notFound((c) => { - return c.json( - { - errors: "Not Found", - }, - 404 - ); + return c.json({ errors: "Not Found" }, 404); }); app.onError(async (err, c) => { if (err instanceof HTTPException) { - c.status(err.status); - return c.json({ - errors: err.message, - }); + return c.json({ errors: err.message }, err.status); } else if (err instanceof ZodError) { - c.status(400); - return c.json({ - errors: err.message, - }); + return c.json({ errors: JSON.parse(err.message) }, 400); } else { - c.status(500); - return c.json({ - errors: err.message, - }); + return c.json({ errors: err.message }, 500); } }); @@ -61,17 +46,23 @@ const server: ServerType = serve({ fetch: app.fetch, }); -process.on("SIGINT", gracefulShutdown); -process.on("SIGTERM", gracefulShutdown); -process.on("unhandledRejection", gracefulShutdown); +const events = ["uncaughtException", "SIGINT", "SIGTERM"]; + +events.forEach((eventName) => { + log.info("listening on ", eventName); + process.on(eventName, (...args) => { + gracefulShutdown(); + log.info(`${eventName} was called with args : ${args.join(",")}`); + log.info(`${eventName} signal received: closing HTTP server`); + }); +}); async function gracefulShutdown(): Promise { - logger.info("SIGTERM/SIGINT signal received: closing HTTP server"); - logger.info("Shutting down gracefully..."); + log.info("Shutting down gracefully..."); await prismaClient.$disconnect(); server.close(() => { - logger.info("HTTP server closed"); + log.info("HTTP server closed"); // Close any other connections or resources here process.exit(0); }); From 881f566bdcdfa7151ad424760c8807768c324936 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sat, 4 Jan 2025 21:29:49 +0700 Subject: [PATCH 19/38] fix: user prisma --- .../20250102145130_init/migration.sql | 9 ---- .../20250104111614_init/migration.sql | 42 +++++++++++++++++++ prisma/schema/user.prisma | 10 +++-- 3 files changed, 49 insertions(+), 12 deletions(-) delete mode 100644 prisma/migrations/20250102145130_init/migration.sql create mode 100644 prisma/migrations/20250104111614_init/migration.sql diff --git a/prisma/migrations/20250102145130_init/migration.sql b/prisma/migrations/20250102145130_init/migration.sql deleted file mode 100644 index a0ab8ae..0000000 --- a/prisma/migrations/20250102145130_init/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ --- CreateTable -CREATE TABLE `users` ( - `id` INTEGER NOT NULL AUTO_INCREMENT, - `email` VARCHAR(191) NOT NULL, - `name` VARCHAR(191) NULL, - - UNIQUE INDEX `users_email_key`(`email`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/prisma/migrations/20250104111614_init/migration.sql b/prisma/migrations/20250104111614_init/migration.sql new file mode 100644 index 0000000..b09df59 --- /dev/null +++ b/prisma/migrations/20250104111614_init/migration.sql @@ -0,0 +1,42 @@ +-- CreateTable +CREATE TABLE `addresses` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `street` VARCHAR(255) NULL, + `city` VARCHAR(100) NULL, + `province` VARCHAR(100) NULL, + `country` VARCHAR(100) NOT NULL, + `postal_code` VARCHAR(10) NOT NULL, + `contact_id` INTEGER NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `contacts` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `first_name` VARCHAR(100) NOT NULL, + `last_name` VARCHAR(100) NULL, + `email` VARCHAR(100) NULL, + `phone` VARCHAR(20) NULL, + `username` VARCHAR(100) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `users` ( + `email` VARCHAR(191) NOT NULL, + `username` VARCHAR(100) NOT NULL, + `password` VARCHAR(100) NOT NULL, + `name` VARCHAR(100) NOT NULL, + `token` VARCHAR(100) NULL, + + UNIQUE INDEX `users_email_key`(`email`), + PRIMARY KEY (`username`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `addresses` ADD CONSTRAINT `addresses_contact_id_fkey` FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `contacts` ADD CONSTRAINT `contacts_username_fkey` FOREIGN KEY (`username`) REFERENCES `users`(`username`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema/user.prisma b/prisma/schema/user.prisma index 164fbc0..431f1a2 100644 --- a/prisma/schema/user.prisma +++ b/prisma/schema/user.prisma @@ -1,7 +1,11 @@ model User { - id Int @id @default(autoincrement()) - email String @unique - name String? + email String @unique + username String @id @db.VarChar(100) + password String @db.VarChar(100) + name String @db.VarChar(100) + token String? @db.VarChar(100) + + contacts Contact[] @@map("users") } From 56bcbe0347ebe0eb15c94a1ce63b88a4c613c42b Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sat, 4 Jan 2025 21:30:19 +0700 Subject: [PATCH 20/38] fix: logger export name to log --- src/config/database.ts | 14 +++++++------- src/config/logger.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/config/database.ts b/src/config/database.ts index a5245b9..75dadee 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -1,4 +1,4 @@ -import { logger } from "./logger"; +import { log } from "./logger"; import { Prisma, PrismaClient } from "@prisma/client"; export const prismaClient = new PrismaClient({ @@ -23,18 +23,18 @@ export const prismaClient = new PrismaClient({ }); /* istanbul ignore next */ prismaClient.$on("error", (event: Prisma.LogEvent): void => { - logger.error(event); + log.error(event); }); /* istanbul ignore next */ prismaClient.$on("warn", (e: Prisma.LogEvent): void => { - logger.warn(e); + log.warn(e); }); prismaClient.$on("info", (e: Prisma.LogEvent): void => { - logger.info(e); + log.info(e); }); prismaClient.$on("query", (e: Prisma.QueryEvent): void => { - logger.verbose(`Query: ${e.query}`); - logger.verbose(`Params: ${e.params}`); - logger.verbose(`Duration: ${e.duration} ms`); + log.verbose(`Query: ${e.query}`); + log.verbose(`Params: ${e.params}`); + log.verbose(`Duration: ${e.duration} ms`); }); diff --git a/src/config/logger.ts b/src/config/logger.ts index 9e4ae90..fecc15a 100644 --- a/src/config/logger.ts +++ b/src/config/logger.ts @@ -55,7 +55,7 @@ if (logLevel == "warn") { ); } -export const logger = createLogger({ +export const log = createLogger({ format: combine( timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), logsFormat, From bf6b3985b294a89cc0bb982e962a8bf185756ac8 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sat, 4 Jan 2025 21:58:05 +0700 Subject: [PATCH 21/38] add: User Register --- src/controller/user-controller.ts | 25 ++++++++++++ src/main.ts | 3 +- src/model/app-model.ts | 5 +++ src/model/user-model.ts | 23 +++++++++++ src/service/user-service.ts | 40 ++++++++++++++++++ src/validation/user-validation.ts | 10 +++++ test/test-util.ts | 26 ++++++++++++ test/user.test.ts | 68 +++++++++++++++++++++++++++++++ 8 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 src/controller/user-controller.ts create mode 100644 src/model/app-model.ts create mode 100644 src/model/user-model.ts create mode 100644 src/service/user-service.ts create mode 100644 src/validation/user-validation.ts create mode 100644 test/test-util.ts create mode 100644 test/user.test.ts diff --git a/src/controller/user-controller.ts b/src/controller/user-controller.ts new file mode 100644 index 0000000..b4bb1df --- /dev/null +++ b/src/controller/user-controller.ts @@ -0,0 +1,25 @@ +import { Hono } from "hono"; +import { type RegisterUserRequest } from "../model/user-model"; +import { UserService } from "../service/user-service"; +import type { ApplicationVariables } from "../model/app-model"; +import { log } from "../config/logger"; + +export const userController = new Hono<{ Variables: ApplicationVariables }>(); + +userController.post("/users", async (c) => { + const text = await c.req.text(); + let request: RegisterUserRequest; + + try { + request = JSON.parse(text) as RegisterUserRequest; + } catch { + return c.json({ error: "invalid json" }, 400); + } + + const response = await UserService.register(request); + log.info("Registering user", response); + + return c.json({ + data: response, + }); +}); diff --git a/src/main.ts b/src/main.ts index fd2bf0f..bb656bb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,7 @@ config(); const port: number = Number(process.env.API_PORT ?? 3030); -const app = new Hono().basePath("/api"); +export const app = new Hono().basePath("/api"); app.get("/", (c) => { return c.json( @@ -49,7 +49,6 @@ const server: ServerType = serve({ const events = ["uncaughtException", "SIGINT", "SIGTERM"]; events.forEach((eventName) => { - log.info("listening on ", eventName); process.on(eventName, (...args) => { gracefulShutdown(); log.info(`${eventName} was called with args : ${args.join(",")}`); diff --git a/src/model/app-model.ts b/src/model/app-model.ts new file mode 100644 index 0000000..ae0f110 --- /dev/null +++ b/src/model/app-model.ts @@ -0,0 +1,5 @@ +import type { User } from "@prisma/client"; + +export type ApplicationVariables = { + user: User; +}; diff --git a/src/model/user-model.ts b/src/model/user-model.ts new file mode 100644 index 0000000..195505d --- /dev/null +++ b/src/model/user-model.ts @@ -0,0 +1,23 @@ +import type { User } from "@prisma/client"; + +export type RegisterUserRequest = { + email: string; + username: string; + password: string; + name: string; +}; + +export type UserResponse = { + email: string; + username: string; + name: string; + token?: string; +}; + +export function toUserResponse(user: User): UserResponse { + return { + email: user.email, + name: user.name, + username: user.username, + }; +} diff --git a/src/service/user-service.ts b/src/service/user-service.ts new file mode 100644 index 0000000..f7aa7d0 --- /dev/null +++ b/src/service/user-service.ts @@ -0,0 +1,40 @@ +import { + type RegisterUserRequest, + toUserResponse, + type UserResponse, +} from "../model/user-model"; +import { UserValidation } from "../validation/user-validation"; +import { prismaClient } from "../config/database"; +import { HTTPException } from "hono/http-exception"; +import { log } from "../config/logger"; + +export class UserService { + static async register(request: RegisterUserRequest): Promise { + log.info("Registering user", request); + + request = UserValidation.REGISTER.parse(request); + + const totalUserWithSameUsername = await prismaClient.user.count({ + where: { + username: request.username, + }, + }); + + if (totalUserWithSameUsername != 0) { + throw new HTTPException(400, { + message: "Username already exists", + }); + } + + request.password = await Bun.password.hash(request.password, { + algorithm: "bcrypt", + cost: 10, + }); + + const user = await prismaClient.user.create({ + data: request, + }); + + return toUserResponse(user); + } +} diff --git a/src/validation/user-validation.ts b/src/validation/user-validation.ts new file mode 100644 index 0000000..90d0a91 --- /dev/null +++ b/src/validation/user-validation.ts @@ -0,0 +1,10 @@ +import { z, ZodType } from "zod"; + +export class UserValidation { + static readonly REGISTER: ZodType = z.object({ + username: z.string().min(1).max(100), + password: z.string().min(1).max(100), + email: z.string().email(), + name: z.string().min(1).max(100), + }); +} diff --git a/test/test-util.ts b/test/test-util.ts new file mode 100644 index 0000000..156ba11 --- /dev/null +++ b/test/test-util.ts @@ -0,0 +1,26 @@ +import { prismaClient } from "../src/config/database"; + +export class UserTest { + static async create() { + await prismaClient.user.create({ + data: { + username: "test", + name: "test", + email: "test@gmail.com", + password: await Bun.password.hash("test", { + algorithm: "bcrypt", + cost: 10, + }), + token: "test", + }, + }); + } + + static async delete() { + await prismaClient.user.deleteMany({ + where: { + username: "test", + }, + }); + } +} diff --git a/test/user.test.ts b/test/user.test.ts new file mode 100644 index 0000000..2869962 --- /dev/null +++ b/test/user.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, afterEach } from "bun:test"; +import { log as logger } from "../src/config/logger"; +import { UserTest } from "./test-util"; +import { app } from "../src/main"; + +describe("POST /api/users", () => { + afterEach(async () => { + await UserTest.delete(); + }); + + it("should reject register new user if request is invalid", async () => { + const response = await app.request("/api/users", { + method: "post", + body: JSON.stringify({ + username: "", + password: "", + name: "", + email: "", + }), + }); + + const body = await response.json(); + logger.debug(body); + + expect(response.status).toBe(400); + expect(body.errors).toBeDefined(); + }); + + it("should reject register new user if username already exists", async () => { + await UserTest.create(); + + const response = await app.request("/api/users", { + method: "post", + body: JSON.stringify({ + username: "test", + password: "test", + name: "test", + email: "test@gmail.com", + }), + }); + + const body = await response.json(); + logger.debug(body); + + expect(response.status).toBe(400); + expect(body.errors).toBeDefined(); + }); + + it("should register new user success", async () => { + const response = await app.request("/api/users", { + method: "post", + body: JSON.stringify({ + username: "test", + password: "test", + name: "test", + email: "test@gmail.com", + }), + }); + + const body = await response.json(); + logger.debug(body); + + expect(response.status).toBe(200); + expect(body.data).toBeDefined(); + expect(body.data.username).toBe("test"); + expect(body.data.name).toBe("test"); + }); +}); From cbb0030edfccd709cbfa5d45058abcc0fda1d6ac Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 05:45:22 +0700 Subject: [PATCH 22/38] fix: .env now using Bun.env --- bun.lockb | Bin 62662 -> 62313 bytes package.json | 1 - src/config/logger.ts | 3 +-- src/main.ts | 9 +++------ 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/bun.lockb b/bun.lockb index fa068e0de447a2a491ca6d535b9d76147e8528c2..88026bbd3017e345e32a0b787cd0daa5d955dea2 100755 GIT binary patch delta 9008 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 LlG|I(M@9WF3V;6m delta 9131 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 diff --git a/package.json b/package.json index b6a9078..f0324d4 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "dependencies": { "@hono/node-server": "^1.13.7", "@prisma/client": "^6.1.0", - "dotenv": "^16.4.7", "hono": "^4.6.15", "winston": "^3.17.0", "zod": "^3.24.1" diff --git a/src/config/logger.ts b/src/config/logger.ts index fecc15a..b1b2f83 100644 --- a/src/config/logger.ts +++ b/src/config/logger.ts @@ -1,4 +1,3 @@ -import "dotenv/config"; import { createLogger, format, transports } from "winston"; const { combine, timestamp, printf, colorize } = format; @@ -7,7 +6,7 @@ const logsFormat = printf(({ level, message, timestamp }) => { return `${timestamp}|[${level.toUpperCase()}]|${message}|`; }); -const logLevel: string = process.env.LOG_LEVEL ?? "warn"; +const logLevel: string = Bun.env.LOG_LEVEL ?? "warn"; const todayDate: string = new Date().toISOString().slice(0, 10); const logFolder: string = `./logs/${todayDate.replace(/-/g, "")}/${todayDate}`; diff --git a/src/main.ts b/src/main.ts index bb656bb..e17654c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,5 @@ import { Hono } from "hono"; import { ZodError } from "zod"; -import { config } from "dotenv"; import pkg from "../package.json"; import { log } from "./config/logger"; import { prismaClient } from "./config/database"; @@ -8,9 +7,7 @@ import { HTTPException } from "hono/http-exception"; import { serve, type ServerType } from "@hono/node-server"; import { userController } from "./controller/user-controller"; -config(); - -const port: number = Number(process.env.API_PORT ?? 3030); +const port: number = Number(Bun.env.API_PORT ?? 3030); export const app = new Hono().basePath("/api"); @@ -18,8 +15,8 @@ app.get("/", (c) => { return c.json( { message: pkg.description ?? "KAnggara Web APP", - version: process.env.APP_VERSION ?? pkg.version ?? "0.0.1", - stability: process.env.APP_STAB ?? "Developer-Preview", + version: Bun.env.APP_VERSION ?? pkg.version ?? "0.0.1", + stability: Bun.env.APP_STAB ?? "Developer-Preview", }, 200 ); From 9c13292af4a2a8602983878eec09c4dfd110ecab Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 05:56:01 +0700 Subject: [PATCH 23/38] fix: Bun test Config --- bunfig.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 bunfig.toml diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..7c1ef59 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,3 @@ +[test] +coverage = true +coverageThreshold = 0.80 From d8e1bcd3ce707bbfa028186d1cfe67c7527689ca Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 06:01:15 +0700 Subject: [PATCH 24/38] add: logger test --- src/config/logger.ts | 10 ------- test/logger.test.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 test/logger.test.ts diff --git a/src/config/logger.ts b/src/config/logger.ts index b1b2f83..1bc8b55 100644 --- a/src/config/logger.ts +++ b/src/config/logger.ts @@ -22,24 +22,14 @@ const myTransports: ( filename: `${logFolder}-warn.log`, level: "warn", }), - new transports.File({ - filename: `${logFolder}-info.log`, - level: "info", - }), - new transports.File({ - filename: `${logFolder}-query.log`, - level: "verbose", - }), ]; -/* istanbul ignore next */ if (logLevel == "warn") { myTransports.push( new transports.Console({ level: "warn", }) ); - /* istanbul ignore next */ } else if (logLevel == "info") { myTransports.push( new transports.Console({ diff --git a/test/logger.test.ts b/test/logger.test.ts new file mode 100644 index 0000000..921b899 --- /dev/null +++ b/test/logger.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect, spyOn } from "bun:test"; +import { log } from "../src/config/logger"; + +describe("Logger Test", () => { + it("Log error Test", async () => { + const logMsg = "This is an error log Test"; + const errorLog = spyOn(log, "error"); + expect(errorLog).toHaveBeenCalledTimes(0); + log.error(logMsg); + expect(errorLog).toHaveBeenCalledTimes(1); + expect(errorLog.mock.calls).toEqual([[logMsg]]); + }); + + it("Log warn Test", async () => { + const logMsg = "This is an warn log Test"; + const warnLog = spyOn(log, "warn"); + expect(warnLog).toHaveBeenCalledTimes(0); + log.warn(logMsg); + expect(warnLog).toHaveBeenCalledTimes(1); + expect(warnLog.mock.calls).toEqual([[logMsg]]); + }); + + it("Log info Test", async () => { + const logMsg = "This is an info log Test"; + const infoLog = spyOn(log, "info"); + expect(infoLog).toHaveBeenCalledTimes(0); + log.info(logMsg); + expect(infoLog).toHaveBeenCalledTimes(1); + expect(infoLog.mock.calls).toEqual([[logMsg]]); + }); + + it("Log debug Test", async () => { + const logMsg = "This is an debug log Test"; + const debugLog = spyOn(log, "debug"); + expect(debugLog).toHaveBeenCalledTimes(0); + log.debug(logMsg); + expect(debugLog).toHaveBeenCalledTimes(1); + expect(debugLog.mock.calls).toEqual([[logMsg]]); + }); + + it("Log http Test", async () => { + const logMsg = "This is an http log Test"; + const httpLog = spyOn(log, "http"); + expect(httpLog).toHaveBeenCalledTimes(0); + log.http(logMsg); + expect(httpLog).toHaveBeenCalledTimes(1); + expect(httpLog.mock.calls).toEqual([[logMsg]]); + }); + + it("Log verbose Test", async () => { + const logMsg = "This is an verbose log Test"; + const verboseLog = spyOn(log, "verbose"); + expect(verboseLog).toHaveBeenCalledTimes(0); + log.verbose(logMsg); + expect(verboseLog).toHaveBeenCalledTimes(1); + expect(verboseLog.mock.calls).toEqual([[logMsg]]); + }); + + it("Log silly Test", async () => { + const logMsg = "This is an silly log Test"; + const sillyLog = spyOn(log, "silly"); + expect(sillyLog).toHaveBeenCalledTimes(0); + log.silly(logMsg); + expect(sillyLog).toHaveBeenCalledTimes(1); + expect(sillyLog.mock.calls).toEqual([[logMsg]]); + }); +}); From 037d6a703a18ea983d0ba1767f6c0693aef67a70 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 06:03:34 +0700 Subject: [PATCH 25/38] add: user register test --- test/user.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/user.test.ts b/test/user.test.ts index 2869962..7f3103a 100644 --- a/test/user.test.ts +++ b/test/user.test.ts @@ -26,6 +26,19 @@ describe("POST /api/users", () => { expect(body.errors).toBeDefined(); }); + it("should reject register new user if request is invalid 2", async () => { + const response = await app.request("/api/users", { + method: "post", + body: JSON.stringify({}), + }); + + const body = await response.json(); + logger.debug(body); + + expect(response.status).toBe(400); + expect(body.errors).toBeDefined(); + }); + it("should reject register new user if username already exists", async () => { await UserTest.create(); From 057792f501cf164a01aa3821a2ce29262c2b2e0e Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 06:19:55 +0700 Subject: [PATCH 26/38] add: user LOGIN --- src/controller/user-controller.ts | 21 +++++++++- src/model/user-model.ts | 5 +++ src/service/user-service.ts | 43 +++++++++++++++++++- src/validation/user-validation.ts | 5 +++ test/user.test.ts | 66 ++++++++++++++++++++++++++++--- 5 files changed, 131 insertions(+), 9 deletions(-) diff --git a/src/controller/user-controller.ts b/src/controller/user-controller.ts index b4bb1df..e380ed1 100644 --- a/src/controller/user-controller.ts +++ b/src/controller/user-controller.ts @@ -1,8 +1,12 @@ import { Hono } from "hono"; -import { type RegisterUserRequest } from "../model/user-model"; +import { + type LoginUserRequest, + type RegisterUserRequest, +} from "../model/user-model"; import { UserService } from "../service/user-service"; import type { ApplicationVariables } from "../model/app-model"; import { log } from "../config/logger"; +import { HTTPException } from "hono/http-exception"; export const userController = new Hono<{ Variables: ApplicationVariables }>(); @@ -13,7 +17,10 @@ userController.post("/users", async (c) => { try { request = JSON.parse(text) as RegisterUserRequest; } catch { - return c.json({ error: "invalid json" }, 400); + log.error("Invalid json request " + text); + throw new HTTPException(400, { + message: "invalid json", + }); } const response = await UserService.register(request); @@ -23,3 +30,13 @@ userController.post("/users", async (c) => { data: response, }); }); + +userController.post("/users/login", async (c) => { + const request = (await c.req.json()) as LoginUserRequest; + + const response = await UserService.login(request); + + return c.json({ + data: response, + }); +}); diff --git a/src/model/user-model.ts b/src/model/user-model.ts index 195505d..73c50b8 100644 --- a/src/model/user-model.ts +++ b/src/model/user-model.ts @@ -7,6 +7,11 @@ export type RegisterUserRequest = { name: string; }; +export type LoginUserRequest = { + username: string; + password: string; +}; + export type UserResponse = { email: string; username: string; diff --git a/src/service/user-service.ts b/src/service/user-service.ts index f7aa7d0..5224e74 100644 --- a/src/service/user-service.ts +++ b/src/service/user-service.ts @@ -1,4 +1,5 @@ import { + type LoginUserRequest, type RegisterUserRequest, toUserResponse, type UserResponse, @@ -10,7 +11,7 @@ import { log } from "../config/logger"; export class UserService { static async register(request: RegisterUserRequest): Promise { - log.info("Registering user", request); + log.info("Registering user " + JSON.stringify(request)); request = UserValidation.REGISTER.parse(request); @@ -37,4 +38,44 @@ export class UserService { return toUserResponse(user); } + + static async login(request: LoginUserRequest): Promise { + request = UserValidation.LOGIN.parse(request); + + let user = await prismaClient.user.findUnique({ + where: { + username: request.username, + }, + }); + + if (!user) { + throw new HTTPException(401, { + message: "Username or password is wrong", + }); + } + + const isPasswordValid = await Bun.password.verify( + request.password, + user.password, + "bcrypt" + ); + if (!isPasswordValid) { + throw new HTTPException(401, { + message: "Username or password is wrong", + }); + } + + user = await prismaClient.user.update({ + where: { + username: request.username, + }, + data: { + token: crypto.randomUUID(), + }, + }); + + const response = toUserResponse(user); + response.token = user.token!; + return response; + } } diff --git a/src/validation/user-validation.ts b/src/validation/user-validation.ts index 90d0a91..6c3c67b 100644 --- a/src/validation/user-validation.ts +++ b/src/validation/user-validation.ts @@ -7,4 +7,9 @@ export class UserValidation { email: z.string().email(), name: z.string().min(1).max(100), }); + + static readonly LOGIN: ZodType = z.object({ + username: z.string().min(1).max(100), + password: z.string().min(1).max(100), + }); } diff --git a/test/user.test.ts b/test/user.test.ts index 7f3103a..de366d9 100644 --- a/test/user.test.ts +++ b/test/user.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, afterEach } from "bun:test"; +import { describe, it, expect, afterEach, beforeEach } from "bun:test"; import { log as logger } from "../src/config/logger"; import { UserTest } from "./test-util"; import { app } from "../src/main"; @@ -20,7 +20,7 @@ describe("POST /api/users", () => { }); const body = await response.json(); - logger.debug(body); + logger.error(JSON.stringify(body)); expect(response.status).toBe(400); expect(body.errors).toBeDefined(); @@ -29,11 +29,10 @@ describe("POST /api/users", () => { it("should reject register new user if request is invalid 2", async () => { const response = await app.request("/api/users", { method: "post", - body: JSON.stringify({}), }); const body = await response.json(); - logger.debug(body); + logger.error(JSON.stringify(body)); expect(response.status).toBe(400); expect(body.errors).toBeDefined(); @@ -53,7 +52,7 @@ describe("POST /api/users", () => { }); const body = await response.json(); - logger.debug(body); + logger.error(JSON.stringify(body)); expect(response.status).toBe(400); expect(body.errors).toBeDefined(); @@ -71,7 +70,7 @@ describe("POST /api/users", () => { }); const body = await response.json(); - logger.debug(body); + logger.info(JSON.stringify(body)); expect(response.status).toBe(200); expect(body.data).toBeDefined(); @@ -79,3 +78,58 @@ describe("POST /api/users", () => { expect(body.data.name).toBe("test"); }); }); + +describe("POST /api/users/login", () => { + beforeEach(async () => { + await UserTest.create(); + }); + + afterEach(async () => { + await UserTest.delete(); + }); + + it("should be able to login", async () => { + const response = await app.request("/api/users/login", { + method: "post", + body: JSON.stringify({ + username: "test", + password: "test", + }), + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data.token).toBeDefined(); + }); + + it("should be rejected if username is wrong", async () => { + const response = await app.request("/api/users/login", { + method: "post", + body: JSON.stringify({ + username: "salah", + password: "test", + }), + }); + + expect(response.status).toBe(401); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should be rejected if password is wrong", async () => { + const response = await app.request("/api/users/login", { + method: "post", + body: JSON.stringify({ + username: "test", + password: "salah", + }), + }); + + expect(response.status).toBe(401); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); +}); From b916e3ff3ae48a36c69dcdeb3d7b4074342727dd Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 06:24:54 +0700 Subject: [PATCH 27/38] add: GET current user --- src/controller/user-controller.ts | 13 ++++++++ src/middleware/auth-middleware.ts | 11 +++++++ src/service/user-service.ts | 29 +++++++++++++++++- src/validation/user-validation.ts | 2 ++ test/user.test.ts | 51 +++++++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/middleware/auth-middleware.ts diff --git a/src/controller/user-controller.ts b/src/controller/user-controller.ts index e380ed1..cd8b2f6 100644 --- a/src/controller/user-controller.ts +++ b/src/controller/user-controller.ts @@ -2,9 +2,12 @@ import { Hono } from "hono"; import { type LoginUserRequest, type RegisterUserRequest, + toUserResponse, } from "../model/user-model"; import { UserService } from "../service/user-service"; import type { ApplicationVariables } from "../model/app-model"; +import type { User } from "@prisma/client"; +import { authMiddleware } from "../middleware/auth-middleware"; import { log } from "../config/logger"; import { HTTPException } from "hono/http-exception"; @@ -40,3 +43,13 @@ userController.post("/users/login", async (c) => { data: response, }); }); + +userController.use(authMiddleware); + +userController.get("/users/current", async (c) => { + const user = c.get("user") as User; + + return c.json({ + data: toUserResponse(user), + }); +}); diff --git a/src/middleware/auth-middleware.ts b/src/middleware/auth-middleware.ts new file mode 100644 index 0000000..f878296 --- /dev/null +++ b/src/middleware/auth-middleware.ts @@ -0,0 +1,11 @@ +import type { MiddlewareHandler } from "hono"; +import { UserService } from "../service/user-service"; + +export const authMiddleware: MiddlewareHandler = async (c, next) => { + const token = c.req.header("Authorization"); + const user = await UserService.get(token); + + c.set("user", user); + + await next(); +}; diff --git a/src/service/user-service.ts b/src/service/user-service.ts index 5224e74..4e6e987 100644 --- a/src/service/user-service.ts +++ b/src/service/user-service.ts @@ -7,6 +7,7 @@ import { import { UserValidation } from "../validation/user-validation"; import { prismaClient } from "../config/database"; import { HTTPException } from "hono/http-exception"; +import type { User } from "@prisma/client"; import { log } from "../config/logger"; export class UserService { @@ -50,7 +51,7 @@ export class UserService { if (!user) { throw new HTTPException(401, { - message: "Username or password is wrong", + message: "Username or Password is wrong", }); } @@ -78,4 +79,30 @@ export class UserService { response.token = user.token!; return response; } + + static async get(token: string | undefined | null): Promise { + const result = UserValidation.TOKEN.safeParse(token); + + if (result.error) { + throw new HTTPException(401, { + message: "Unauthorized", + }); + } + + token = result.data; + + const user = await prismaClient.user.findFirst({ + where: { + token: token, + }, + }); + + if (!user) { + throw new HTTPException(401, { + message: "Unauthorized", + }); + } + + return user; + } } diff --git a/src/validation/user-validation.ts b/src/validation/user-validation.ts index 6c3c67b..ff3ba82 100644 --- a/src/validation/user-validation.ts +++ b/src/validation/user-validation.ts @@ -12,4 +12,6 @@ export class UserValidation { username: z.string().min(1).max(100), password: z.string().min(1).max(100), }); + + static readonly TOKEN: ZodType = z.string().min(1); } diff --git a/test/user.test.ts b/test/user.test.ts index de366d9..92f2ee2 100644 --- a/test/user.test.ts +++ b/test/user.test.ts @@ -133,3 +133,54 @@ describe("POST /api/users/login", () => { expect(body.errors).toBeDefined(); }); }); + +describe("GET /api/users/current", () => { + beforeEach(async () => { + await UserTest.create(); + }); + + afterEach(async () => { + await UserTest.delete(); + }); + + it("should be able to get user", async () => { + const response = await app.request("/api/users/current", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(body.data.username).toBe("test"); + expect(body.data.name).toBe("test"); + }); + + it("should not be able to get user if token is invalid", async () => { + const response = await app.request("/api/users/current", { + method: "get", + headers: { + Authorization: "salah", + }, + }); + + expect(response.status).toBe(401); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should not be able to get user if there is no Authorization header", async () => { + const response = await app.request("/api/users/current", { + method: "get", + }); + + expect(response.status).toBe(401); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); +}); From 9d06a12ff23e98feba8e53ef1a4b68fd62c0e6b7 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 06:29:28 +0700 Subject: [PATCH 28/38] add: PATCH update user --- src/controller/user-controller.ts | 12 +++++ src/model/user-model.ts | 5 ++ src/service/user-service.ts | 28 ++++++++++++ src/validation/user-validation.ts | 5 ++ test/user.test.ts | 76 +++++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+) diff --git a/src/controller/user-controller.ts b/src/controller/user-controller.ts index cd8b2f6..fc2c34d 100644 --- a/src/controller/user-controller.ts +++ b/src/controller/user-controller.ts @@ -3,6 +3,7 @@ import { type LoginUserRequest, type RegisterUserRequest, toUserResponse, + type UpdateUserRequest, } from "../model/user-model"; import { UserService } from "../service/user-service"; import type { ApplicationVariables } from "../model/app-model"; @@ -53,3 +54,14 @@ userController.get("/users/current", async (c) => { data: toUserResponse(user), }); }); + +userController.patch("/users/current", async (c) => { + const user = c.get("user") as User; + const request = (await c.req.json()) as UpdateUserRequest; + + const response = await UserService.update(user, request); + + return c.json({ + data: response, + }); +}); diff --git a/src/model/user-model.ts b/src/model/user-model.ts index 73c50b8..2b8e170 100644 --- a/src/model/user-model.ts +++ b/src/model/user-model.ts @@ -12,6 +12,11 @@ export type LoginUserRequest = { password: string; }; +export type UpdateUserRequest = { + password?: string; + name?: string; +}; + export type UserResponse = { email: string; username: string; diff --git a/src/service/user-service.ts b/src/service/user-service.ts index 4e6e987..5037da7 100644 --- a/src/service/user-service.ts +++ b/src/service/user-service.ts @@ -2,6 +2,7 @@ import { type LoginUserRequest, type RegisterUserRequest, toUserResponse, + type UpdateUserRequest, type UserResponse, } from "../model/user-model"; import { UserValidation } from "../validation/user-validation"; @@ -105,4 +106,31 @@ export class UserService { return user; } + + static async update( + user: User, + request: UpdateUserRequest + ): Promise { + request = UserValidation.UPDATE.parse(request); + + if (request.name) { + user.name = request.name; + } + + if (request.password) { + user.password = await Bun.password.hash(request.password, { + algorithm: "bcrypt", + cost: 10, + }); + } + + user = await prismaClient.user.update({ + where: { + username: user.username, + }, + data: user, + }); + + return toUserResponse(user); + } } diff --git a/src/validation/user-validation.ts b/src/validation/user-validation.ts index ff3ba82..bf464e6 100644 --- a/src/validation/user-validation.ts +++ b/src/validation/user-validation.ts @@ -14,4 +14,9 @@ export class UserValidation { }); static readonly TOKEN: ZodType = z.string().min(1); + + static readonly UPDATE: ZodType = z.object({ + password: z.string().min(1).max(100).optional(), + name: z.string().min(1).max(100).optional(), + }); } diff --git a/test/user.test.ts b/test/user.test.ts index 92f2ee2..6ba1e0c 100644 --- a/test/user.test.ts +++ b/test/user.test.ts @@ -184,3 +184,79 @@ describe("GET /api/users/current", () => { expect(body.errors).toBeDefined(); }); }); + +describe("PATCH /api/users/current", () => { + beforeEach(async () => { + await UserTest.create(); + }); + + afterEach(async () => { + await UserTest.delete(); + }); + + it("should be rejected if request is invalid", async () => { + const response = await app.request("/api/users/current", { + method: "patch", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + name: "", + password: "", + }), + }); + + expect(response.status).toBe(400); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should be able to update name", async () => { + const response = await app.request("/api/users/current", { + method: "patch", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + name: "IDScript", + }), + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + logger.error(body); + expect(body.data).toBeDefined(); + expect(body.data.name).toBe("IDScript"); + }); + + it("should be able to update password", async () => { + let response = await app.request("/api/users/current", { + method: "patch", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + password: "baru", + }), + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + logger.error(body); + expect(body.data).toBeDefined(); + expect(body.data.name).toBe("test"); + + response = await app.request("/api/users/login", { + method: "post", + body: JSON.stringify({ + username: "test", + password: "baru", + }), + }); + + expect(response.status).toBe(200); + }); +}); From 42a24c6265d5104bd840fe51af5abfaa85b95e97 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 06:30:49 +0700 Subject: [PATCH 29/38] add: DELETE user logout --- src/controller/user-controller.ts | 10 +++++++ src/service/user-service.ts | 13 ++++++++ test/user.test.ts | 50 +++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/controller/user-controller.ts b/src/controller/user-controller.ts index fc2c34d..6d93d23 100644 --- a/src/controller/user-controller.ts +++ b/src/controller/user-controller.ts @@ -65,3 +65,13 @@ userController.patch("/users/current", async (c) => { data: response, }); }); + +userController.delete("/users/current", async (c) => { + const user = c.get("user") as User; + + const response = await UserService.logout(user); + + return c.json({ + data: response, + }); +}); diff --git a/src/service/user-service.ts b/src/service/user-service.ts index 5037da7..fd3021d 100644 --- a/src/service/user-service.ts +++ b/src/service/user-service.ts @@ -133,4 +133,17 @@ export class UserService { return toUserResponse(user); } + + static async logout(user: User): Promise { + await prismaClient.user.update({ + where: { + username: user.username, + }, + data: { + token: null, + }, + }); + + return true; + } } diff --git a/test/user.test.ts b/test/user.test.ts index 6ba1e0c..f0dbaa6 100644 --- a/test/user.test.ts +++ b/test/user.test.ts @@ -226,7 +226,7 @@ describe("PATCH /api/users/current", () => { expect(response.status).toBe(200); const body = await response.json(); - logger.error(body); + logger.error(JSON.stringify(body)); expect(body.data).toBeDefined(); expect(body.data.name).toBe("IDScript"); }); @@ -245,7 +245,7 @@ describe("PATCH /api/users/current", () => { expect(response.status).toBe(200); const body = await response.json(); - logger.error(body); + logger.error(JSON.stringify(body)); expect(body.data).toBeDefined(); expect(body.data.name).toBe("test"); @@ -260,3 +260,49 @@ describe("PATCH /api/users/current", () => { expect(response.status).toBe(200); }); }); + +describe("DELETE /api/users/current", () => { + beforeEach(async () => { + await UserTest.create(); + }); + + afterEach(async () => { + await UserTest.delete(); + }); + + it("should be able to logout", async () => { + const response = await app.request("/api/users/current", { + method: "delete", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data).toBe(true); + }); + + it("should not be able to logout", async () => { + let response = await app.request("/api/users/current", { + method: "delete", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data).toBe(true); + + response = await app.request("/api/users/current", { + method: "delete", + headers: { + Authorization: "test", + }, + }); + expect(response.status).toBe(401); + }); +}); From 07307df78912a56d4588f1329ed2d92307b8c744 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 06:56:36 +0700 Subject: [PATCH 30/38] add: contact --- .../migration.sql | 0 prisma/schema/contact.prisma | 14 + src/controller/contact-controller.ts | 70 +++ src/main.ts | 2 + src/model/contact-model.ts | 42 ++ src/model/page-model.ts | 10 + src/service/contact-service.ts | 158 ++++++ src/validation/contact-validation.ts | 30 ++ test/contact.test.ts | 456 ++++++++++++++++++ test/test-util.ts | 37 ++ 10 files changed, 819 insertions(+) rename prisma/migrations/{20250104111614_init => 20250104234059_init}/migration.sql (100%) create mode 100644 prisma/schema/contact.prisma create mode 100644 src/controller/contact-controller.ts create mode 100644 src/model/contact-model.ts create mode 100644 src/model/page-model.ts create mode 100644 src/service/contact-service.ts create mode 100644 src/validation/contact-validation.ts create mode 100644 test/contact.test.ts diff --git a/prisma/migrations/20250104111614_init/migration.sql b/prisma/migrations/20250104234059_init/migration.sql similarity index 100% rename from prisma/migrations/20250104111614_init/migration.sql rename to prisma/migrations/20250104234059_init/migration.sql diff --git a/prisma/schema/contact.prisma b/prisma/schema/contact.prisma new file mode 100644 index 0000000..39dfb12 --- /dev/null +++ b/prisma/schema/contact.prisma @@ -0,0 +1,14 @@ +model Contact { + id Int @id @default(autoincrement()) + first_name String @db.VarChar(100) + last_name String? @db.VarChar(100) + email String? @db.VarChar(100) + phone String? @db.VarChar(20) + + username String @db.VarChar(100) + + user User @relation(fields: [username], references: [username]) + addresses Address[] + + @@map("contacts") +} diff --git a/src/controller/contact-controller.ts b/src/controller/contact-controller.ts new file mode 100644 index 0000000..f4cf2e5 --- /dev/null +++ b/src/controller/contact-controller.ts @@ -0,0 +1,70 @@ +import { Hono } from "hono"; +import type { ApplicationVariables } from "../model/app-model"; +import { authMiddleware } from "../middleware/auth-middleware"; +import type { User } from "@prisma/client"; +import { ContactService } from "../service/contact-service"; +import type { + CreateContactRequest, + SearchContactRequest, + UpdateContactRequest, +} from "../model/contact-model"; + +export const contactController = new Hono<{ + Variables: ApplicationVariables; +}>(); +contactController.use(authMiddleware); + +contactController.post("/contacts", async (c) => { + const user = c.get("user") as User; + const request = (await c.req.json()) as CreateContactRequest; + const response = await ContactService.create(user, request); + + return c.json({ + data: response, + }); +}); + +contactController.get("/contacts/:id", async (c) => { + const user = c.get("user") as User; + const contactId = Number(c.req.param("id")); + const response = await ContactService.get(user, contactId); + + return c.json({ + data: response, + }); +}); + +contactController.put("/contacts/:id", async (c) => { + const user = c.get("user") as User; + const contactId = Number(c.req.param("id")); + const request = (await c.req.json()) as UpdateContactRequest; + request.id = contactId; + const response = await ContactService.update(user, request); + + return c.json({ + data: response, + }); +}); + +contactController.delete("/contacts/:id", async (c) => { + const user = c.get("user") as User; + const contactId = Number(c.req.param("id")); + const response = await ContactService.delete(user, contactId); + + return c.json({ + data: response, + }); +}); + +contactController.get("/contacts", async (c) => { + const user = c.get("user") as User; + const request: SearchContactRequest = { + name: c.req.query("name"), + email: c.req.query("email"), + phone: c.req.query("phone"), + page: c.req.query("page") ? Number(c.req.query("page")) : 1, + size: c.req.query("size") ? Number(c.req.query("size")) : 10, + }; + const response = await ContactService.search(user, request); + return c.json(response); +}); diff --git a/src/main.ts b/src/main.ts index e17654c..2afeffa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import { prismaClient } from "./config/database"; import { HTTPException } from "hono/http-exception"; import { serve, type ServerType } from "@hono/node-server"; import { userController } from "./controller/user-controller"; +import { contactController } from "./controller/contact-controller"; const port: number = Number(Bun.env.API_PORT ?? 3030); @@ -23,6 +24,7 @@ app.get("/", (c) => { }); app.route("/", userController); +app.route("/", contactController); app.notFound((c) => { return c.json({ errors: "Not Found" }, 404); diff --git a/src/model/contact-model.ts b/src/model/contact-model.ts new file mode 100644 index 0000000..8ca6973 --- /dev/null +++ b/src/model/contact-model.ts @@ -0,0 +1,42 @@ +import type { Contact } from "@prisma/client"; + +export type CreateContactRequest = { + first_name: string; + last_name?: string; + email?: string; + phone?: string; +}; + +export type ContactResponse = { + id: number; + first_name: string; + last_name?: string | null; + email?: string | null; + phone?: string | null; +}; + +export type UpdateContactRequest = { + id: number; + first_name: string; + last_name?: string; + email?: string; + phone?: string; +}; + +export type SearchContactRequest = { + name?: string; + phone?: string; + email?: string; + page: number; + size: number; +}; + +export function toContactResponse(contact: Contact): ContactResponse { + return { + id: contact.id, + first_name: contact.first_name, + last_name: contact.last_name, + email: contact.email, + phone: contact.phone, + }; +} diff --git a/src/model/page-model.ts b/src/model/page-model.ts new file mode 100644 index 0000000..133d95b --- /dev/null +++ b/src/model/page-model.ts @@ -0,0 +1,10 @@ +export type Paging = { + current_page: number; + total_page: number; + size: number; +}; + +export type Pageable = { + data: Array; + paging: Paging; +}; diff --git a/src/service/contact-service.ts b/src/service/contact-service.ts new file mode 100644 index 0000000..387ca0c --- /dev/null +++ b/src/service/contact-service.ts @@ -0,0 +1,158 @@ +import type { Contact, User } from "@prisma/client"; +import type { + ContactResponse, + CreateContactRequest, + SearchContactRequest, + UpdateContactRequest, +} from "../model/contact-model"; +import { toContactResponse } from "../model/contact-model"; +import { ContactValidation } from "../validation/contact-validation"; +import { prismaClient } from "../config/database"; +import { HTTPException } from "hono/http-exception"; +import type { Pageable } from "../model/page-model"; + +export class ContactService { + static async create( + user: User, + request: CreateContactRequest + ): Promise { + request = ContactValidation.CREATE.parse(request); + + let data = { + ...request, + ...{ username: user.username }, + }; + + const contact = await prismaClient.contact.create({ + data: data, + }); + + return toContactResponse(contact); + } + + static async get(user: User, contactId: number): Promise { + contactId = ContactValidation.GET.parse(contactId); + const contact = await this.contactMustExists(user, contactId); + return toContactResponse(contact); + } + + static async contactMustExists( + user: User, + contactId: number + ): Promise { + const contact = await prismaClient.contact.findFirst({ + where: { + id: contactId, + username: user.username, + }, + }); + + if (!contact) { + throw new HTTPException(404, { + message: "Contact is not found", + }); + } + + return contact; + } + + static async update( + user: User, + request: UpdateContactRequest + ): Promise { + request = ContactValidation.UPDATE.parse(request); + await this.contactMustExists(user, request.id); + + const contact = await prismaClient.contact.update({ + where: { + username: user.username, + id: request.id, + }, + data: request, + }); + + return toContactResponse(contact); + } + + static async delete(user: User, contactId: number): Promise { + contactId = ContactValidation.DELETE.parse(contactId); + await this.contactMustExists(user, contactId); + + await prismaClient.contact.delete({ + where: { + username: user.username, + id: contactId, + }, + }); + + return true; + } + + static async search( + user: User, + request: SearchContactRequest + ): Promise> { + request = ContactValidation.SEARCH.parse(request); + + const filters = []; + if (request.name) { + filters.push({ + OR: [ + { + first_name: { + contains: request.name, + }, + }, + { + last_name: { + contains: request.name, + }, + }, + ], + }); + } + + if (request.email) { + filters.push({ + email: { + contains: request.email, + }, + }); + } + + if (request.phone) { + filters.push({ + phone: { + contains: request.phone, + }, + }); + } + + const skip = (request.page - 1) * request.size; + + const contacts = await prismaClient.contact.findMany({ + where: { + username: user.username, + AND: filters, + }, + take: request.size, + skip: skip, + }); + + const total = await prismaClient.contact.count({ + where: { + username: user.username, + AND: filters, + }, + }); + + return { + data: contacts.map((contact) => toContactResponse(contact)), + paging: { + current_page: request.page, + size: request.size, + total_page: Math.ceil(total / request.size), + }, + }; + } +} diff --git a/src/validation/contact-validation.ts b/src/validation/contact-validation.ts new file mode 100644 index 0000000..6a7ede8 --- /dev/null +++ b/src/validation/contact-validation.ts @@ -0,0 +1,30 @@ +import { z, ZodType } from "zod"; + +export class ContactValidation { + static readonly CREATE: ZodType = z.object({ + first_name: z.string().min(1).max(100), + last_name: z.string().min(1).max(100).optional(), + email: z.string().min(1).max(100).email().optional(), + phone: z.string().min(1).max(20).optional(), + }); + + static readonly GET: ZodType = z.number().positive(); + + static readonly UPDATE: ZodType = z.object({ + id: z.number().positive(), + first_name: z.string().min(1).max(100), + last_name: z.string().min(1).max(100).optional(), + email: z.string().min(1).max(100).email().optional(), + phone: z.string().min(1).max(20).optional(), + }); + + static readonly DELETE: ZodType = z.number().positive(); + + static readonly SEARCH: ZodType = z.object({ + name: z.string().min(1).optional(), + email: z.string().min(1).optional(), + phone: z.string().min(1).optional(), + page: z.number().min(1).positive(), + size: z.number().min(1).max(100).positive(), + }); +} diff --git a/test/contact.test.ts b/test/contact.test.ts new file mode 100644 index 0000000..18b6a38 --- /dev/null +++ b/test/contact.test.ts @@ -0,0 +1,456 @@ +import { expect, describe, it, beforeEach, afterEach } from "bun:test"; +import { ContactTest, UserTest } from "./test-util"; +import { app } from "../src/main"; + +describe("POST /api/contacts", () => { + beforeEach(async () => { + await ContactTest.deleteAll(); + await UserTest.create(); + }); + + afterEach(async () => { + await ContactTest.deleteAll(); + await UserTest.delete(); + }); + + it("should rejected if token is not valid", async () => { + const response = await app.request("/api/contacts", { + method: "post", + headers: { + Authorization: "salah", + }, + body: JSON.stringify({ + first_name: "", + }), + }); + + expect(response.status).toBe(401); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should rejected if contact is invalid", async () => { + const response = await app.request("/api/contacts", { + method: "post", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + first_name: "", + }), + }); + + expect(response.status).toBe(400); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should success if contact is valid (only first_name)", async () => { + const response = await app.request("/api/contacts", { + method: "post", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + first_name: "IDScript", + }), + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(body.data.first_name).toBe("IDScript"); + expect(body.data.last_name).toBeNull(); + expect(body.data.email).toBeNull(); + expect(body.data.phone).toBeNull(); + }); + + it("should success if contact is valid (full data)", async () => { + const response = await app.request("/api/contacts", { + method: "post", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + first_name: "IDScript", + last_name: "IDScript", + email: "IDScript@gmail.com", + phone: "23424234324", + }), + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(body.data.first_name).toBe("IDScript"); + expect(body.data.last_name).toBe("IDScript"); + expect(body.data.email).toBe("IDScript@gmail.com"); + expect(body.data.phone).toBe("23424234324"); + }); +}); + +describe("GET /api/contacts/{id}", () => { + beforeEach(async () => { + await ContactTest.deleteAll(); + await UserTest.create(); + await ContactTest.create(); + }); + + afterEach(async () => { + await ContactTest.deleteAll(); + await UserTest.delete(); + }); + + it("should get 404 if contact is not found", async () => { + const contact = await ContactTest.get(); + + const response = await app.request("/api/contacts/" + (contact.id + 1), { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(404); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should success if contact is exists", async () => { + const contact = await ContactTest.get(); + + const response = await app.request("/api/contacts/" + contact.id, { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(body.data.first_name).toBe(contact.first_name); + expect(body.data.last_name).toBe(contact.last_name); + expect(body.data.email).toBe(contact.email); + expect(body.data.phone).toBe(contact.phone); + expect(body.data.id).toBe(contact.id); + }); +}); + +describe("PUT /api/contacts/{id}", () => { + beforeEach(async () => { + await ContactTest.deleteAll(); + await UserTest.create(); + await ContactTest.create(); + }); + + afterEach(async () => { + await ContactTest.deleteAll(); + await UserTest.delete(); + }); + + it("should rejected update contact if request is invalid", async () => { + const contact = await ContactTest.get(); + + const response = await app.request("/api/contacts/" + contact.id, { + method: "put", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + first_name: "", + }), + }); + + expect(response.status).toBe(400); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should rejected update contact if id is not found", async () => { + const contact = await ContactTest.get(); + + const response = await app.request("/api/contacts/" + (contact.id + 1), { + method: "put", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + first_name: "IDScript", + }), + }); + + expect(response.status).toBe(404); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should success update contact if request is valid", async () => { + const contact = await ContactTest.get(); + + const response = await app.request("/api/contacts/" + contact.id, { + method: "put", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + first_name: "IDScript", + last_name: "IDScript", + email: "IDScript@gmail.com", + phone: "1231234", + }), + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(body.data.first_name).toBe("IDScript"); + expect(body.data.last_name).toBe("IDScript"); + expect(body.data.email).toBe("IDScript@gmail.com"); + expect(body.data.phone).toBe("1231234"); + }); +}); + +describe("DELETE /api/contacts/{id}", () => { + beforeEach(async () => { + await ContactTest.deleteAll(); + await UserTest.create(); + await ContactTest.create(); + }); + + afterEach(async () => { + await ContactTest.deleteAll(); + await UserTest.delete(); + }); + + it("should rejected if contact id is not found", async () => { + const contact = await ContactTest.get(); + + const response = await app.request("/api/contacts/" + (contact.id + 1), { + method: "delete", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(404); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should success if contact is exists", async () => { + const contact = await ContactTest.get(); + + const response = await app.request("/api/contacts/" + contact.id, { + method: "delete", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data).toBe(true); + }); +}); + +describe("GET /api/contacts", () => { + beforeEach(async () => { + await ContactTest.deleteAll(); + await UserTest.create(); + await ContactTest.createMany(25); + }); + + afterEach(async () => { + await ContactTest.deleteAll(); + await UserTest.delete(); + }); + + it("should be able to search contact", async () => { + const response = await app.request("/api/contacts", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data.length).toBe(10); + expect(body.paging.current_page).toBe(1); + expect(body.paging.size).toBe(10); + expect(body.paging.total_page).toBe(3); + }); + + it("should be able to search contact using name", async () => { + let response = await app.request("/api/contacts?name=IDS", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + let body = await response.json(); + expect(body.data.length).toBe(10); + expect(body.paging.current_page).toBe(1); + expect(body.paging.size).toBe(10); + expect(body.paging.total_page).toBe(3); + + response = await app.request("/api/contacts?name=script", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + body = await response.json(); + expect(body.data.length).toBe(10); + expect(body.paging.current_page).toBe(1); + expect(body.paging.size).toBe(10); + expect(body.paging.total_page).toBe(3); + }); + + it("should be able to search contact using email", async () => { + const response = await app.request("/api/contacts?email=gmail", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data.length).toBe(10); + expect(body.paging.current_page).toBe(1); + expect(body.paging.size).toBe(10); + expect(body.paging.total_page).toBe(3); + }); + + it("should be able to search contact using phone", async () => { + const response = await app.request("/api/contacts?phone=31", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data.length).toBe(10); + expect(body.paging.current_page).toBe(1); + expect(body.paging.size).toBe(10); + expect(body.paging.total_page).toBe(3); + }); + + it("should be able to search without result", async () => { + let response = await app.request("/api/contacts?name=budi", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + let body = await response.json(); + expect(body.data.length).toBe(0); + expect(body.paging.current_page).toBe(1); + expect(body.paging.size).toBe(10); + expect(body.paging.total_page).toBe(0); + + response = await app.request("/api/contacts?email=gakada", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + body = await response.json(); + expect(body.data.length).toBe(0); + expect(body.paging.current_page).toBe(1); + expect(body.paging.size).toBe(10); + expect(body.paging.total_page).toBe(0); + + response = await app.request("/api/contacts?phone=gakada", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + body = await response.json(); + expect(body.data.length).toBe(0); + expect(body.paging.current_page).toBe(1); + expect(body.paging.size).toBe(10); + expect(body.paging.total_page).toBe(0); + }); + + it("should be able to search with paging", async () => { + let response = await app.request("/api/contacts?size=5", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + let body = await response.json(); + expect(body.data.length).toBe(5); + expect(body.paging.current_page).toBe(1); + expect(body.paging.size).toBe(5); + expect(body.paging.total_page).toBe(5); + + response = await app.request("/api/contacts?size=5&page=2", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + body = await response.json(); + expect(body.data.length).toBe(5); + expect(body.paging.current_page).toBe(2); + expect(body.paging.size).toBe(5); + expect(body.paging.total_page).toBe(5); + + response = await app.request("/api/contacts?size=5&page=100", { + method: "get", + headers: { + Authorization: "test", + }, + }); + + expect(response.status).toBe(200); + + body = await response.json(); + expect(body.data.length).toBe(0); + expect(body.paging.current_page).toBe(100); + expect(body.paging.size).toBe(5); + expect(body.paging.total_page).toBe(5); + }); +}); diff --git a/test/test-util.ts b/test/test-util.ts index 156ba11..dd01bc7 100644 --- a/test/test-util.ts +++ b/test/test-util.ts @@ -1,4 +1,5 @@ import { prismaClient } from "../src/config/database"; +import type { Contact } from "@prisma/client"; export class UserTest { static async create() { @@ -24,3 +25,39 @@ export class UserTest { }); } } + +export class ContactTest { + static async deleteAll() { + await prismaClient.contact.deleteMany({ + where: { + username: "test", + }, + }); + } + + static async create() { + await prismaClient.contact.create({ + data: { + first_name: "IDScript", + last_name: "IDScript", + email: "test@gmail.com", + phone: "123123", + username: "test", + }, + }); + } + + static async createMany(n: number) { + for (let i = 0; i < n; i++) { + await this.create(); + } + } + + static async get(): Promise { + return prismaClient.contact.findFirstOrThrow({ + where: { + username: "test", + }, + }); + } +} From 56cd3846ebd229d719c9655e37e4f31b76ef3515 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 07:58:49 +0700 Subject: [PATCH 31/38] add: address --- prisma/schema/address.prisma | 14 ++ src/controller/address-controller.ts | 85 +++++++ src/main.ts | 2 + src/model/address-model.ts | 54 ++++ src/service/address-service.ts | 116 +++++++++ src/validation/address-validation.ts | 36 +++ test/address.test.ts | 362 +++++++++++++++++++++++++++ test/test-util.ts | 38 ++- 8 files changed, 706 insertions(+), 1 deletion(-) create mode 100644 prisma/schema/address.prisma create mode 100644 src/controller/address-controller.ts create mode 100644 src/model/address-model.ts create mode 100644 src/service/address-service.ts create mode 100644 src/validation/address-validation.ts create mode 100644 test/address.test.ts diff --git a/prisma/schema/address.prisma b/prisma/schema/address.prisma new file mode 100644 index 0000000..1a7b35e --- /dev/null +++ b/prisma/schema/address.prisma @@ -0,0 +1,14 @@ +model Address { + id Int @id @default(autoincrement()) + street String? @db.VarChar(255) + city String? @db.VarChar(100) + province String? @db.VarChar(100) + country String @db.VarChar(100) + postal_code String @db.VarChar(10) + + contact_id Int + + contact Contact @relation(fields: [contact_id], references: [id]) + + @@map("addresses") +} diff --git a/src/controller/address-controller.ts b/src/controller/address-controller.ts new file mode 100644 index 0000000..25a7e64 --- /dev/null +++ b/src/controller/address-controller.ts @@ -0,0 +1,85 @@ +import { Hono } from "hono"; +import type { ApplicationVariables } from "../model/app-model"; +import { authMiddleware } from "../middleware/auth-middleware"; +import type { User } from "@prisma/client"; +import type { + CreateAddressRequest, + GetAddressRequest, + ListAddressRequest, + RemoveAddressRequest, + UpdateAddressRequest, +} from "../model/address-model"; +import { AddressService } from "../service/address-service"; + +export const addressController = new Hono<{ + Variables: ApplicationVariables; +}>(); +addressController.use(authMiddleware); + +addressController.post("/contacts/:id/addresses", async (c) => { + const user = c.get("user") as User; + const contactId = Number(c.req.param("id")); + const request = (await c.req.json()) as CreateAddressRequest; + request.contact_id = contactId; + const response = await AddressService.create(user, request); + return c.json({ + data: response, + }); +}); + +addressController.get( + "/contacts/:contact_id/addresses/:address_id", + async (c) => { + const user = c.get("user") as User; + const request: GetAddressRequest = { + contact_id: Number(c.req.param("contact_id")), + id: Number(c.req.param("address_id")), + }; + const response = await AddressService.get(user, request); + return c.json({ + data: response, + }); + } +); + +addressController.put( + "/contacts/:contact_id/addresses/:address_id", + async (c) => { + const user = c.get("user") as User; + const contactId = Number(c.req.param("contact_id")); + const addressId = Number(c.req.param("address_id")); + const request = (await c.req.json()) as UpdateAddressRequest; + request.contact_id = contactId; + request.id = addressId; + const response = await AddressService.update(user, request); + return c.json({ + data: response, + }); + } +); + +addressController.delete( + "/contacts/:contact_id/addresses/:address_id", + async (c) => { + const user = c.get("user") as User; + const request: RemoveAddressRequest = { + id: Number(c.req.param("address_id")), + contact_id: Number(c.req.param("contact_id")), + }; + const response = await AddressService.remove(user, request); + return c.json({ + data: response, + }); + } +); + +addressController.get("/contacts/:contact_id/addresses", async (c) => { + const user = c.get("user") as User; + const request: ListAddressRequest = { + contact_id: Number(c.req.param("contact_id")), + }; + const response = await AddressService.list(user, request); + return c.json({ + data: response, + }); +}); diff --git a/src/main.ts b/src/main.ts index 2afeffa..0e32ae9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { HTTPException } from "hono/http-exception"; import { serve, type ServerType } from "@hono/node-server"; import { userController } from "./controller/user-controller"; import { contactController } from "./controller/contact-controller"; +import { addressController } from "./controller/address-controller"; const port: number = Number(Bun.env.API_PORT ?? 3030); @@ -25,6 +26,7 @@ app.get("/", (c) => { app.route("/", userController); app.route("/", contactController); +app.route("/", addressController); app.notFound((c) => { return c.json({ errors: "Not Found" }, 404); diff --git a/src/model/address-model.ts b/src/model/address-model.ts new file mode 100644 index 0000000..5c43636 --- /dev/null +++ b/src/model/address-model.ts @@ -0,0 +1,54 @@ +import type { Address } from "@prisma/client"; + +export type CreateAddressRequest = { + contact_id: number; + street?: string; + city?: string; + province?: string; + country: string; + postal_code: string; +}; + +export type AddressResponse = { + id: number; + street?: string | null; + city?: string | null; + province?: string | null; + country: string; + postal_code: string; +}; + +export type GetAddressRequest = { + contact_id: number; + id: number; +}; + +export type UpdateAddressRequest = { + id: number; + contact_id: number; + street?: string; + city?: string; + province?: string; + country: string; + postal_code: string; +}; + +export type RemoveAddressRequest = { + contact_id: number; + id: number; +}; + +export type ListAddressRequest = { + contact_id: number; +}; + +export function toAddressResponse(address: Address): AddressResponse { + return { + id: address.id, + street: address.street, + city: address.city, + province: address.province, + country: address.country, + postal_code: address.postal_code, + }; +} diff --git a/src/service/address-service.ts b/src/service/address-service.ts new file mode 100644 index 0000000..da9bb18 --- /dev/null +++ b/src/service/address-service.ts @@ -0,0 +1,116 @@ +import type { Address, User } from "@prisma/client"; +import { + toAddressResponse, + type AddressResponse, + type CreateAddressRequest, + type GetAddressRequest, + type ListAddressRequest, + type RemoveAddressRequest, + type UpdateAddressRequest, +} from "../model/address-model"; +import { AddressValidation } from "../validation/address-validation"; +import { ContactService } from "./contact-service"; +import { prismaClient } from "../config/database"; +import { HTTPException } from "hono/http-exception"; + +export class AddressService { + static async create( + user: User, + request: CreateAddressRequest + ): Promise { + request = AddressValidation.CREATE.parse(request); + await ContactService.contactMustExists(user, request.contact_id); + + const address = await prismaClient.address.create({ + data: request, + }); + + return toAddressResponse(address); + } + + static async get( + user: User, + request: GetAddressRequest + ): Promise { + request = AddressValidation.GET.parse(request); + await ContactService.contactMustExists(user, request.contact_id); + const address = await this.addressMustExists( + request.contact_id, + request.id + ); + + return toAddressResponse(address); + } + + static async addressMustExists( + contactId: number, + addressId: number + ): Promise
{ + const address = await prismaClient.address.findFirst({ + where: { + contact_id: contactId, + id: addressId, + }, + }); + + if (!address) { + throw new HTTPException(404, { + message: "Address is not found", + }); + } + return address; + } + + static async update( + user: User, + request: UpdateAddressRequest + ): Promise { + request = AddressValidation.UPDATE.parse(request); + await ContactService.contactMustExists(user, request.contact_id); + await this.addressMustExists(request.contact_id, request.id); + + const address = await prismaClient.address.update({ + where: { + id: request.id, + contact_id: request.contact_id, + }, + data: request, + }); + + return toAddressResponse(address); + } + + static async remove( + user: User, + request: RemoveAddressRequest + ): Promise { + request = AddressValidation.REMOVE.parse(request); + await ContactService.contactMustExists(user, request.contact_id); + await this.addressMustExists(request.contact_id, request.id); + + await prismaClient.address.delete({ + where: { + id: request.id, + contact_id: request.contact_id, + }, + }); + + return true; + } + + static async list( + user: User, + request: ListAddressRequest + ): Promise> { + request = AddressValidation.LIST.parse(request); + await ContactService.contactMustExists(user, request.contact_id); + + const addresses = await prismaClient.address.findMany({ + where: { + contact_id: request.contact_id, + }, + }); + + return addresses.map((address) => toAddressResponse(address)); + } +} diff --git a/src/validation/address-validation.ts b/src/validation/address-validation.ts new file mode 100644 index 0000000..13fd325 --- /dev/null +++ b/src/validation/address-validation.ts @@ -0,0 +1,36 @@ +import { z, ZodType } from "zod"; + +export class AddressValidation { + static readonly CREATE: ZodType = z.object({ + contact_id: z.number().positive(), + street: z.string().min(1).max(255).optional(), + city: z.string().min(1).max(100).optional(), + province: z.string().min(1).max(100).optional(), + country: z.string().min(1).max(100), + postal_code: z.string().min(1).max(10), + }); + + static readonly GET: ZodType = z.object({ + contact_id: z.number().positive(), + id: z.number().positive(), + }); + + static readonly UPDATE: ZodType = z.object({ + id: z.number().positive(), + contact_id: z.number().positive(), + street: z.string().min(1).max(255).optional(), + city: z.string().min(1).max(100).optional(), + province: z.string().min(1).max(100).optional(), + country: z.string().min(1).max(100), + postal_code: z.string().min(1).max(10), + }); + + static readonly REMOVE: ZodType = z.object({ + contact_id: z.number().positive(), + id: z.number().positive(), + }); + + static readonly LIST: ZodType = z.object({ + contact_id: z.number().positive(), + }); +} diff --git a/test/address.test.ts b/test/address.test.ts new file mode 100644 index 0000000..b8e0b68 --- /dev/null +++ b/test/address.test.ts @@ -0,0 +1,362 @@ +import { expect, describe, it, beforeEach, afterEach } from "bun:test"; +import { AddressTest, ContactTest, UserTest } from "./test-util"; +import { app } from "../src/main"; + +describe("POST /api/contacts/{id}/addresses", () => { + beforeEach(async () => { + await AddressTest.deleteAll(); + await ContactTest.deleteAll(); + await UserTest.delete(); + + await UserTest.create(); + await ContactTest.create(); + }); + + afterEach(async () => { + await AddressTest.deleteAll(); + await ContactTest.deleteAll(); + await UserTest.delete(); + }); + + it("should rejected if request is not valid", async () => { + const contact = await ContactTest.get(); + const response = await app.request( + "/api/contacts/" + contact.id + "/addresses", + { + method: "post", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + country: "", + postal_code: "", + }), + } + ); + + expect(response.status).toBe(400); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should rejected if contact is not found", async () => { + const contact = await ContactTest.get(); + const response = await app.request( + "/api/contacts/" + (contact.id + 1) + "/addresses", + { + method: "post", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + country: "Indonesia", + postal_code: "1213", + }), + } + ); + + expect(response.status).toBe(404); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should success if request is valid", async () => { + const contact = await ContactTest.get(); + const response = await app.request( + "/api/contacts/" + contact.id + "/addresses", + { + method: "post", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + street: "Jalan", + city: "Kota", + province: "Provinsi", + country: "Indonesia", + postal_code: "12345", + }), + } + ); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(body.data.id).toBeDefined(); + expect(body.data.street).toBe("Jalan"); + expect(body.data.city).toBe("Kota"); + expect(body.data.province).toBe("Provinsi"); + expect(body.data.country).toBe("Indonesia"); + expect(body.data.postal_code).toBe("12345"); + }); +}); + +describe("GET /api/contacts/{contactId}/addresses/{addressId}", () => { + beforeEach(async () => { + await AddressTest.deleteAll(); + await ContactTest.deleteAll(); + await UserTest.delete(); + + await UserTest.create(); + await ContactTest.create(); + await AddressTest.create(); + }); + + afterEach(async () => { + await AddressTest.deleteAll(); + await ContactTest.deleteAll(); + await UserTest.delete(); + }); + + it("should rejected if address is not found", async () => { + const contact = await ContactTest.get(); + const address = await AddressTest.get(); + const response = await app.request( + "/api/contacts/" + contact.id + "/addresses/" + (address.id + 1), + { + method: "get", + headers: { + Authorization: "test", + }, + } + ); + + expect(response.status).toBe(404); + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should success if address is exists", async () => { + const contact = await ContactTest.get(); + const address = await AddressTest.get(); + const response = await app.request( + "/api/contacts/" + contact.id + "/addresses/" + address.id, + { + method: "get", + headers: { + Authorization: "test", + }, + } + ); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(body.data.id).toBeDefined(); + expect(body.data.street).toBe(address.street); + expect(body.data.city).toBe(address.city); + expect(body.data.province).toBe(address.province); + expect(body.data.country).toBe(address.country); + expect(body.data.postal_code).toBe(address.postal_code); + }); +}); + +describe("PUT /api/contacts/{contactId}/addresses/{addressId}", () => { + beforeEach(async () => { + await AddressTest.deleteAll(); + await ContactTest.deleteAll(); + await UserTest.delete(); + + await UserTest.create(); + await ContactTest.create(); + await AddressTest.create(); + }); + + afterEach(async () => { + await AddressTest.deleteAll(); + await ContactTest.deleteAll(); + await UserTest.delete(); + }); + + it("should rejected if request is invalid", async () => { + const contact = await ContactTest.get(); + const address = await AddressTest.get(); + const response = await app.request( + "/api/contacts/" + contact.id + "/addresses/" + address.id, + { + method: "put", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + country: "", + postal_code: "", + }), + } + ); + + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should rejected if address is not found", async () => { + const contact = await ContactTest.get(); + const address = await AddressTest.get(); + const response = await app.request( + "/api/contacts/" + contact.id + "/addresses/" + (address.id + 1), + { + method: "put", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + country: "Indonesia", + postal_code: "12345", + }), + } + ); + + expect(response.status).toBe(404); + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should success if request is valid", async () => { + const contact = await ContactTest.get(); + const address = await AddressTest.get(); + const response = await app.request( + "/api/contacts/" + contact.id + "/addresses/" + address.id, + { + method: "put", + headers: { + Authorization: "test", + }, + body: JSON.stringify({ + street: "A", + city: "B", + province: "C", + country: "Malaysia", + postal_code: "9999", + }), + } + ); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(body.data.id).toBe(address.id); + expect(body.data.street).toBe("A"); + expect(body.data.city).toBe("B"); + expect(body.data.province).toBe("C"); + expect(body.data.country).toBe("Malaysia"); + expect(body.data.postal_code).toBe("9999"); + }); +}); + +describe("DELETE /api/contacts/{contactId}/addresses/{addressId}", () => { + beforeEach(async () => { + await AddressTest.deleteAll(); + await ContactTest.deleteAll(); + await UserTest.delete(); + + await UserTest.create(); + await ContactTest.create(); + await AddressTest.create(); + }); + + afterEach(async () => { + await AddressTest.deleteAll(); + await ContactTest.deleteAll(); + await UserTest.delete(); + }); + + it("should rejected if address is not exists", async () => { + const contact = await ContactTest.get(); + const address = await AddressTest.get(); + const response = await app.request( + "/api/contacts/" + contact.id + "/addresses/" + (address.id + 1), + { + method: "delete", + headers: { + Authorization: "test", + }, + } + ); + + expect(response.status).toBe(404); + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should success if address is exists", async () => { + const contact = await ContactTest.get(); + const address = await AddressTest.get(); + const response = await app.request( + "/api/contacts/" + contact.id + "/addresses/" + address.id, + { + method: "delete", + headers: { + Authorization: "test", + }, + } + ); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.data).toBeTrue(); + }); +}); + +describe("DELETE /api/contacts/{contactId}/addresses/{addressId}", () => { + beforeEach(async () => { + await AddressTest.deleteAll(); + await ContactTest.deleteAll(); + await UserTest.delete(); + + await UserTest.create(); + await ContactTest.create(); + await AddressTest.create(); + }); + + afterEach(async () => { + await AddressTest.deleteAll(); + await ContactTest.deleteAll(); + await UserTest.delete(); + }); + + it("should rejected if contact id is not found", async () => { + const contact = await ContactTest.get(); + const response = await app.request( + "/api/contacts/" + (contact.id + 1) + "/addresses", + { + method: "get", + headers: { + Authorization: "test", + }, + } + ); + + expect(response.status).toBe(404); + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("should success if contact is exists", async () => { + const contact = await ContactTest.get(); + const address = await AddressTest.get(); + const response = await app.request( + "/api/contacts/" + contact.id + "/addresses", + { + method: "get", + headers: { + Authorization: "test", + }, + } + ); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.data).toBeDefined(); + expect(body.data.length).toBe(1); + expect(body.data[0].id).toBe(address.id); + expect(body.data[0].street).toBe(address.street); + expect(body.data[0].city).toBe(address.city); + expect(body.data[0].province).toBe(address.province); + expect(body.data[0].country).toBe(address.country); + expect(body.data[0].postal_code).toBe(address.postal_code); + }); +}); diff --git a/test/test-util.ts b/test/test-util.ts index dd01bc7..173fecd 100644 --- a/test/test-util.ts +++ b/test/test-util.ts @@ -1,5 +1,5 @@ import { prismaClient } from "../src/config/database"; -import type { Contact } from "@prisma/client"; +import type { Address, Contact } from "@prisma/client"; export class UserTest { static async create() { @@ -61,3 +61,39 @@ export class ContactTest { }); } } + +export class AddressTest { + static async create() { + const contact = await ContactTest.get(); + await prismaClient.address.create({ + data: { + contact_id: contact.id, + street: "Jalan", + city: "Kota", + province: "Provinsi", + country: "Indonesia", + postal_code: "12345", + }, + }); + } + + static async get(): Promise
{ + return prismaClient.address.findFirstOrThrow({ + where: { + contact: { + username: "test", + }, + }, + }); + } + + static async deleteAll() { + await prismaClient.address.deleteMany({ + where: { + contact: { + username: "test", + }, + }, + }); + } +} From bf59b107bc3cf2f8861f3d58ee57ba77fd2178d6 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 08:03:17 +0700 Subject: [PATCH 32/38] add: github action CI/CD --- .github/workflows/github-ci.yaml | 18 ++++++++++++++++++ package.json | 4 +++- src/config/database.ts | 7 ++----- src/{main.ts => index.ts} | 0 test/address.test.ts | 2 +- test/contact.test.ts | 2 +- test/user.test.ts | 2 +- 7 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/github-ci.yaml rename src/{main.ts => index.ts} (100%) diff --git a/.github/workflows/github-ci.yaml b/.github/workflows/github-ci.yaml new file mode 100644 index 0000000..060230a --- /dev/null +++ b/.github/workflows/github-ci.yaml @@ -0,0 +1,18 @@ +name: "CI/CD ๐Ÿš€" + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + name: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bun src/index.ts + - run: bun run build diff --git a/package.json b/package.json index f0324d4..3fccd9d 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,10 @@ "description": "Restful API by IDScript", "version": "0.1.0", "type": "module", + "main": "src/index.ts", + "module": "src/index.ts", "scripts": { - "dev": "bun run --hot src/main.ts", + "dev": "bun run --hot src/index.ts", "fc": "bunx prettier --write . && bunx eslint --fix . && bunx prettier --check . && pnpm eslint .", "mig": "rm -rf prisma/migrations && bunx prisma generate && bunx prisma migrate dev --name init" }, diff --git a/src/config/database.ts b/src/config/database.ts index 75dadee..72bebe3 100644 --- a/src/config/database.ts +++ b/src/config/database.ts @@ -21,15 +21,12 @@ export const prismaClient = new PrismaClient({ }, ], }); -/* istanbul ignore next */ -prismaClient.$on("error", (event: Prisma.LogEvent): void => { - log.error(event); +prismaClient.$on("error", (e: Prisma.LogEvent): void => { + log.error(e); }); -/* istanbul ignore next */ prismaClient.$on("warn", (e: Prisma.LogEvent): void => { log.warn(e); }); - prismaClient.$on("info", (e: Prisma.LogEvent): void => { log.info(e); }); diff --git a/src/main.ts b/src/index.ts similarity index 100% rename from src/main.ts rename to src/index.ts diff --git a/test/address.test.ts b/test/address.test.ts index b8e0b68..7e9e7d3 100644 --- a/test/address.test.ts +++ b/test/address.test.ts @@ -1,6 +1,6 @@ import { expect, describe, it, beforeEach, afterEach } from "bun:test"; import { AddressTest, ContactTest, UserTest } from "./test-util"; -import { app } from "../src/main"; +import { app } from "../src"; describe("POST /api/contacts/{id}/addresses", () => { beforeEach(async () => { diff --git a/test/contact.test.ts b/test/contact.test.ts index 18b6a38..572527d 100644 --- a/test/contact.test.ts +++ b/test/contact.test.ts @@ -1,6 +1,6 @@ import { expect, describe, it, beforeEach, afterEach } from "bun:test"; import { ContactTest, UserTest } from "./test-util"; -import { app } from "../src/main"; +import { app } from "../src"; describe("POST /api/contacts", () => { beforeEach(async () => { diff --git a/test/user.test.ts b/test/user.test.ts index f0dbaa6..d6c536a 100644 --- a/test/user.test.ts +++ b/test/user.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, afterEach, beforeEach } from "bun:test"; import { log as logger } from "../src/config/logger"; import { UserTest } from "./test-util"; -import { app } from "../src/main"; +import { app } from "../src"; describe("POST /api/users", () => { afterEach(async () => { From 59b5adf764cfb8d075441e2bb2d72d16a98ac01a Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 08:07:32 +0700 Subject: [PATCH 33/38] add: github action CI/CD --- .github/workflows/github-ci.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github-ci.yaml b/.github/workflows/github-ci.yaml index 060230a..471cd60 100644 --- a/.github/workflows/github-ci.yaml +++ b/.github/workflows/github-ci.yaml @@ -8,11 +8,10 @@ on: jobs: build: - name: build + name: unittest runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - run: bun install - - run: bun src/index.ts - - run: bun run build + - run: bun test From 5849fdf53c631fc6d3ef8ce5c3a7badcc32cfc23 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 08:13:37 +0700 Subject: [PATCH 34/38] add: github action CI/CD --- .env.example | 4 +- .github/workflows/github-ci.yaml | 75 ++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 3a1a846..40b6499 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -DATABASE_URL="mysql://root:root@localhost:3306/pakaiwa" +DATABASE_URL="mysql://root:root@localhost:3306/bunapi" APP_VERSION=0.1.0 APP_ENV=production APP_DEBUG=true API_PORT=3030 LOG_LEVEL=info -EXAMPLE_EMAIL=test@pakaiwa.my.id +EXAMPLE_EMAIL=test@idscript.my.id diff --git a/.github/workflows/github-ci.yaml b/.github/workflows/github-ci.yaml index 471cd60..dc2072c 100644 --- a/.github/workflows/github-ci.yaml +++ b/.github/workflows/github-ci.yaml @@ -1,17 +1,74 @@ -name: "CI/CD ๐Ÿš€" +name: Unit Test ๐Ÿงช on: push: - branches: ["main"] + branches: ["main", "dev", "qa"] pull_request: - branches: ["main"] + branches: ["main", "dev", "qa"] jobs: - build: - name: unittest + Format_and_Check: runs-on: ubuntu-latest + strategy: + matrix: + bun-version: [canary, 1.0.0, 1.0.36, 1.1.0, 1.1.16, 1.1.42] + name: Bun ${{ matrix.bun-version }} sample steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun test + - uses: actions/checkout@v4.2.2 + - name: Use Bun ${{ matrix.bun-version }} + uses: oven-sh/setup-bun@v2 + with: + bun-version: ${{ matrix.bun-version }} + - name: Install dependencies + run: bun i + - name: Format and Check code + run: bun run fc + Jest_Test: + runs-on: ubuntu-latest + needs: Format_and_Check + strategy: + matrix: + bun-version: [canary, 1.0.0, 1.0.36, 1.1.0, 1.1.16, 1.1.42] + name: Test with Jest Bun ${{ matrix.bun-version }} + steps: + - name: Start MySQL + run: | + sudo /etc/init.d/mysql start + mysql -e "CREATE DATABASE IF NOT EXISTS bunapi;" -uroot -proot + - uses: actions/checkout@v4.2.2 + - name: Use Bun ${{ matrix.bun-version }} + uses: oven-sh/setup-bun@v2 + with: + bun-version: ${{ matrix.bun-version }} + - name: create .env + run: mv .env.example .env + - name: Install dependencies + run: bun i + - name: Migration + run: bun run mig + - name: Test + run: bun run test + Create_Test_Coverage_Badges: + runs-on: ubuntu-latest + needs: Format_and_Check + name: Test with Jest Bun Latest + steps: + - name: Start MySQL + run: | + sudo /etc/init.d/mysql start + mysql -e "CREATE DATABASE IF NOT EXISTS bunapi;" -uroot -proot + - uses: actions/checkout@v4.2.2 + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + - name: create .env + run: mv .env.example .env + - name: Install dependencies + run: bun i + - name: Migration + run: bun run mig + - name: Test + run: bun run test + - name: Generating coverage badges + uses: jpb06/jest-badges-action@v1.9.18 + with: + branches: main,dev From 325b0fe8265ee23131d5c70d5453687ae46008aa Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 08:16:14 +0700 Subject: [PATCH 35/38] fix: /usr/bin/bash: line 1: pnpm: command not found --- package.json | 2 +- src/service/contact-service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3fccd9d..21f18e5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "module": "src/index.ts", "scripts": { "dev": "bun run --hot src/index.ts", - "fc": "bunx prettier --write . && bunx eslint --fix . && bunx prettier --check . && pnpm eslint .", + "fc": "bunx prettier --write . && bunx eslint --fix . && bunx prettier --check . && bunx eslint .", "mig": "rm -rf prisma/migrations && bunx prisma generate && bunx prisma migrate dev --name init" }, "dependencies": { diff --git a/src/service/contact-service.ts b/src/service/contact-service.ts index 387ca0c..30a13c7 100644 --- a/src/service/contact-service.ts +++ b/src/service/contact-service.ts @@ -18,7 +18,7 @@ export class ContactService { ): Promise { request = ContactValidation.CREATE.parse(request); - let data = { + const data = { ...request, ...{ username: user.username }, }; From ea13423f45a024d6d1988e313ff880873c83a761 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 08:18:07 +0700 Subject: [PATCH 36/38] fix: a package.json script test was not found --- .github/workflows/github-ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-ci.yaml b/.github/workflows/github-ci.yaml index dc2072c..039ef43 100644 --- a/.github/workflows/github-ci.yaml +++ b/.github/workflows/github-ci.yaml @@ -47,7 +47,7 @@ jobs: - name: Migration run: bun run mig - name: Test - run: bun run test + run: bun test Create_Test_Coverage_Badges: runs-on: ubuntu-latest needs: Format_and_Check @@ -67,7 +67,7 @@ jobs: - name: Migration run: bun run mig - name: Test - run: bun run test + run: bun test - name: Generating coverage badges uses: jpb06/jest-badges-action@v1.9.18 with: From eef63336be9fb4042cd2d75fd6d959371d0aed64 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 08:19:19 +0700 Subject: [PATCH 37/38] add: github action CI/CD --- .github/workflows/github-ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github-ci.yaml b/.github/workflows/github-ci.yaml index 039ef43..0c05f5c 100644 --- a/.github/workflows/github-ci.yaml +++ b/.github/workflows/github-ci.yaml @@ -2,9 +2,9 @@ name: Unit Test ๐Ÿงช on: push: - branches: ["main", "dev", "qa"] + branches: ["main", "stg"] pull_request: - branches: ["main", "dev", "qa"] + branches: ["main", "stg"] jobs: Format_and_Check: @@ -71,4 +71,4 @@ jobs: - name: Generating coverage badges uses: jpb06/jest-badges-action@v1.9.18 with: - branches: main,dev + branches: main, stg From 23a3841256f4eef49a3f32de81e7ec2f237d5cc4 Mon Sep 17 00:00:00 2001 From: Call Vin Date: Sun, 5 Jan 2025 09:34:21 +0700 Subject: [PATCH 38/38] fix: test cov config --- bunfig.toml | 3 --- src/index.ts | 1 + test/index.test.ts | 36 ++++++++++++++++++++++++++++++++++++ test/user.test.ts | 10 +++++----- 4 files changed, 42 insertions(+), 8 deletions(-) delete mode 100644 bunfig.toml create mode 100644 test/index.test.ts diff --git a/bunfig.toml b/bunfig.toml deleted file mode 100644 index 7c1ef59..0000000 --- a/bunfig.toml +++ /dev/null @@ -1,3 +0,0 @@ -[test] -coverage = true -coverageThreshold = 0.80 diff --git a/src/index.ts b/src/index.ts index 0e32ae9..1b30acb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ app.route("/", contactController); app.route("/", addressController); app.notFound((c) => { + log.info(`Not Found: ${c.req.url}`); return c.json({ errors: "Not Found" }, 404); }); diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..ddd3c9e --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,36 @@ +import { app } from "../src"; +import { expect, describe, it } from "bun:test"; + +describe("POST", () => { + it("URL Not Found", async () => { + const url = crypto.randomUUID(); + const response = await app.request("/" + url, { + method: "get", + body: JSON.stringify({ + first_name: "OK", + }), + }); + + expect(response.status).toBe(404); + + const body = await response.json(); + expect(body.errors).toBeDefined(); + }); + + it("App Version", async () => { + const response = await app.request("/api", { + method: "get", + body: JSON.stringify({ + first_name: "OK", + }), + }); + + expect(response.status).toBe(200); + + const body = await response.json(); + expect(body.message).toBeDefined(); + expect(body.version).toBeDefined(); + expect(body.stability).toBeDefined(); + expect(body.errors).toBeUndefined(); + }); +}); diff --git a/test/user.test.ts b/test/user.test.ts index d6c536a..4eb69f8 100644 --- a/test/user.test.ts +++ b/test/user.test.ts @@ -20,7 +20,7 @@ describe("POST /api/users", () => { }); const body = await response.json(); - logger.error(JSON.stringify(body)); + logger.debug(JSON.stringify(body)); expect(response.status).toBe(400); expect(body.errors).toBeDefined(); @@ -32,7 +32,7 @@ describe("POST /api/users", () => { }); const body = await response.json(); - logger.error(JSON.stringify(body)); + logger.debug(JSON.stringify(body)); expect(response.status).toBe(400); expect(body.errors).toBeDefined(); @@ -52,7 +52,7 @@ describe("POST /api/users", () => { }); const body = await response.json(); - logger.error(JSON.stringify(body)); + logger.debug(JSON.stringify(body)); expect(response.status).toBe(400); expect(body.errors).toBeDefined(); @@ -226,7 +226,7 @@ describe("PATCH /api/users/current", () => { expect(response.status).toBe(200); const body = await response.json(); - logger.error(JSON.stringify(body)); + logger.debug(JSON.stringify(body)); expect(body.data).toBeDefined(); expect(body.data.name).toBe("IDScript"); }); @@ -245,7 +245,7 @@ describe("PATCH /api/users/current", () => { expect(response.status).toBe(200); const body = await response.json(); - logger.error(JSON.stringify(body)); + logger.debug(JSON.stringify(body)); expect(body.data).toBeDefined(); expect(body.data.name).toBe("test");