feat: add accounts and movies tables in migrations
- Created migration for accounts table with fields: id, username, password, role_id, created_at, updated_at. - Created migration for movies table with fields: id, title, overview, poster_path, backdrop_path, release_date, tmdb_id. refactor: update package.json scripts and dependencies - Changed dev script to use bun instead of tsx. - Added build:migrate script for migration. - Updated devDependencies for bun and oxlint. fix: refactor database connection and migration execution - Updated PgDatabase to use SQL from bun. - Refactored migration execution logic to read SQL files and execute them. feat: implement account creation and validation logic - Updated AccountEntity to use username instead of email. - Added validation for username format and password strength. - Implemented account repository methods for finding by username and inserting accounts. test: add tests for account entity, repository, and service - Created tests for AccountEntity to validate username and password. - Added tests for AccountRepository to ensure correct database interactions. - Implemented tests for AccountService to validate registration and login logic. chore: remove outdated tests and files - Deleted old tests related to email-based account management. - Cleaned up unused imports and files to streamline the codebase.
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"plugins": ["@prettier/plugin-oxc"]
|
"plugins": ["@prettier/plugin-oxc", "prettier-plugin-sql"]
|
||||||
}
|
}
|
||||||
|
|||||||
185
bun.lock
185
bun.lock
@@ -5,21 +5,16 @@
|
|||||||
"name": "nixi-api",
|
"name": "nixi-api",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/zod-validator": "^0.7.3",
|
"@hono/zod-validator": "^0.7.3",
|
||||||
"bcrypt": "^6.0.0",
|
|
||||||
"hono": "^4.9.8",
|
"hono": "^4.9.8",
|
||||||
"pg": "^8.16.3",
|
|
||||||
"zod": "^4.1.11",
|
"zod": "^4.1.11",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@prettier/plugin-oxc": "^0.0.4",
|
"@prettier/plugin-oxc": "^0.0.4",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bun": "1.2.23",
|
||||||
"@types/bun": "latest",
|
|
||||||
"@types/node": "^24.2.1",
|
"@types/node": "^24.2.1",
|
||||||
"@types/pg": "^8.15.5",
|
"oxlint": "^1.19.0",
|
||||||
"oxlint": "^1.16.0",
|
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"rolldown": "^1.0.0-beta.39",
|
"prettier-plugin-sql": "^0.19.2",
|
||||||
"tsx": "^4.20.5",
|
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -31,61 +26,9 @@
|
|||||||
|
|
||||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="],
|
|
||||||
|
|
||||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="],
|
|
||||||
|
|
||||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="],
|
|
||||||
|
|
||||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="],
|
|
||||||
|
|
||||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="],
|
|
||||||
|
|
||||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="],
|
|
||||||
|
|
||||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="],
|
|
||||||
|
|
||||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
|
|
||||||
|
|
||||||
"@hono/zod-validator": ["@hono/zod-validator@0.7.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uYGdgVib3RlGD698WR5dVM0zB3UuPY5vHKXffGUbUh7r4xY+mFIhF3/v4AcQVLrU5CQdBso8BJr4wuVoCrjTuQ=="],
|
"@hono/zod-validator": ["@hono/zod-validator@0.7.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uYGdgVib3RlGD698WR5dVM0zB3UuPY5vHKXffGUbUh7r4xY+mFIhF3/v4AcQVLrU5CQdBso8BJr4wuVoCrjTuQ=="],
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.5", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
"@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.74.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lgq8TJq22eyfojfa2jBFy2m66ckAo7iNRYDdyn9reXYA3I6Wx7tgGWVx1JAp1lO+aUiqdqP/uPlDaETL9tqRcg=="],
|
"@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.74.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lgq8TJq22eyfojfa2jBFy2m66ckAo7iNRYDdyn9reXYA3I6Wx7tgGWVx1JAp1lO+aUiqdqP/uPlDaETL9tqRcg=="],
|
||||||
|
|
||||||
@@ -117,138 +60,80 @@
|
|||||||
|
|
||||||
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.74.0", "", { "os": "win32", "cpu": "x64" }, "sha512-1D3x6iU2apLyfTQHygbdaNbX3nZaHu4yaXpD7ilYpoLo7f0MX0tUuoDrqJyJrVGqvyXgc0uz4yXz9tH9ZZhvvg=="],
|
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.74.0", "", { "os": "win32", "cpu": "x64" }, "sha512-1D3x6iU2apLyfTQHygbdaNbX3nZaHu4yaXpD7ilYpoLo7f0MX0tUuoDrqJyJrVGqvyXgc0uz4yXz9tH9ZZhvvg=="],
|
||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.90.0", "", {}, "sha512-fWvaufWUcLtm/OBKcNmxUkR0kQW5ZKAF0t03BXPqdzpxmnVCmSKzvUDRCOKnSagSfNzG/3ZdKpComH3GMy881g=="],
|
"@oxc-project/types": ["@oxc-project/types@0.74.0", "", {}, "sha512-KOw/RZrVlHGhCXh1RufBFF7Nuo7HdY5w1lRJukM/igIl6x9qtz8QycDvZdzb4qnHO7znrPyo2sJrFJK2eKHgfQ=="],
|
||||||
|
|
||||||
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.16.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-t9sBjbcG15Jgwgw2wY+rtfKEazdkKM/YhcdyjmGYeSjBXaczLfp/gZe03taC2qUHK+t6cxSYNkOLXRLWxaf3tw=="],
|
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.19.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dSozp6FXowhFEjmT0FC/iBWj9KziWfixxaYT367kOXZUyA0hvOzsLsBB780Swr40zvqklUR0d3fbZbziGHRJoQ=="],
|
||||||
|
|
||||||
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.16.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-c9aeLQATeu27TK8gR/p8GfRBsuakx0zs+6UHFq/s8Kux+8tYb3pH1pql/XWUPbxubv48F2MpnD5zgjOrShAgag=="],
|
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.19.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-3OY1km70zTlH6b8K8AHSuaEaa4sntmAcBugMZBaJmHkioia7zxlAQV9xtQ2wsBSDQbBmcf1j5Y0NcHP7fmIZvA=="],
|
||||||
|
|
||||||
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.16.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZoBtxtRHhftbiKKeScpgUKIg4cu9s7rsBPCkjfMCY0uLjhKqm6ShPEaIuP8515+/Csouciz1ViZhbrya5ligAg=="],
|
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.19.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TS9wmx9B/v1f/bNXu3lIEcdNIyS0m0H0+95YIWSTGG3q2cK3FVlyUiiAieZRUzXTN89n6JXtua6dK/TVCqbmkQ=="],
|
||||||
|
|
||||||
"@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.16.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-a/Dys7CTyj1eZIkD59k9Y3lp5YsHBUeZXR7qHTplKb41H+Ivm5OQPf+rfbCBSLMfCPZCeKQPW36GXOSYLNE1uw=="],
|
"@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.19.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-o5RAxQfVEu7LsBUwSjEDNdM8sla8WlLMRULsTP3vgxyy1eLJxo2u+4McKtM9/P2KiZQw3NylDoaxU4Z4j/XeRQ=="],
|
||||||
|
|
||||||
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.16.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rsfv90ytLhl+s7aa8eE8gGwB1XGbiUA2oyUee/RhGRyeoZoe9/hHNtIcE2XndMYlJToROKmGyrTN4MD2c0xxLQ=="],
|
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.19.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QDgAP4TxXsupFEsEGYnaAaKXQQD1lJSi5Htl/b0Vl2xPz8BVBRH+bNDwVGEHVTxT7jdnO2gTEOmfEzOkRJprUQ=="],
|
||||||
|
|
||||||
"@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.16.0", "", { "os": "linux", "cpu": "x64" }, "sha512-djwSL4harw46kdCwaORUvApyE9Y6JSnJ7pF5PHcQlJ7S1IusfjzYljXky4hONPO0otvXWdKq1GpJqhmtM0/xbg=="],
|
"@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.19.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iOQooyYzy7RR2yHNM8oHd2Zw6CdU7/G2Uf5ryFi/cF5NV5zlSH//QSkWwrk/kLF69wKqwE8S8snV7WnRA/tXjA=="],
|
||||||
|
|
||||||
"@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.16.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-lQBfW4hBiQ47P12UAFXyX3RVHlWCSYp6I89YhG+0zoLipxAfyB37P8G8N43T/fkUaleb8lvt0jyNG6jQTkCmhg=="],
|
"@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.19.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-bvgA2fGpdBF/DpB5hZYQzx5fFFiiHxIiPF5zp24czvsIRkezVi9ZH04lCIVkMBxgvKhnU2jLXAn6E1Mbo4QrFw=="],
|
||||||
|
|
||||||
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.16.0", "", { "os": "win32", "cpu": "x64" }, "sha512-B5se3JnM4Xu6uHF78hAY9wdk/sdLFib1YwFsLY6rkQKEMFyi+vMZZlDaAS+s+Dt9q7q881U2OhNznZenJZdPdQ=="],
|
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.19.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PloVn/e1kfMsiH0urM4XIhiY0TdqDjwJlzeX8pIKDmxUsKHsjcU8fmddsZSt7K16C2nR3SQVoso2AIR00mRieA=="],
|
||||||
|
|
||||||
"@prettier/plugin-oxc": ["@prettier/plugin-oxc@0.0.4", "", { "dependencies": { "oxc-parser": "0.74.0" } }, "sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ=="],
|
"@prettier/plugin-oxc": ["@prettier/plugin-oxc@0.0.4", "", { "dependencies": { "oxc-parser": "0.74.0" } }, "sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ=="],
|
||||||
|
|
||||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.39", "", { "os": "android", "cpu": "arm64" }, "sha512-mjraAJQ3VRLPb3BUgVigHvmAYhiBpEeSM0dhvaO6XHtJ0k1o9Ng1Z6Qvlp4/1wDiUf7a10L5c3yleoGZ2r0Maw=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.39", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tnuiLq9vd08KsZeFkFgzCXVKsTgSZGn+YBQjHSEiUvXJy5pfUf82X/YyLCG8P6I+WDd2cgrcLilMBQPZgaNwkg=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.39", "", { "os": "darwin", "cpu": "x64" }, "sha512-wLFoB3ZM4AoeBlsP0eVbPzWfkEgvmnibMQEKUgWRfJnKhUWiSxl0kGdSw1fNYdX3KAqIeA5gPJNvSJmf6g5S3Q=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.39", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wzFZlixF9VMbyi++rHCU4Cy72SH11aBNnkadmvwTAbokwjYHi8NqxQ3/Lx00c700N6kwwuiTsbcGt5DEA9aROw=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.39", "", { "os": "linux", "cpu": "arm" }, "sha512-eVnZcwGbje1uwdFjeQZQ6918RHgGIK7iTC+AoDsgetgAXQmQpnuWYQ9OWa5oTHNQyCkZbMfiHKgpkUPpceMecw=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.39", "", { "os": "linux", "cpu": "arm64" }, "sha512-Td96iRQA0nmRZM6kJ3+LDDKWLh4bl0zqeR+IYxXwPZBw4iXSREzXrcZ3QqgFHqnXPgryIJEW1U1Ebh2xf+b2UA=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.39", "", { "os": "linux", "cpu": "arm64" }, "sha512-bcSIh1TFUoPcexJH+gO1sE6wpSR0j3UpWBnjAwyM1PRKfjtqN4R9Du90ofH5KsR/A35FT3eP4mdnhMDTd5Yt+A=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.39", "", { "os": "linux", "cpu": "x64" }, "sha512-tYEcZdVGovEemh7ELr+VUoezGkuBgRZYvDHHW/HVIw9LQW5HKLtBIGLzFlOfu/Lq5b9FlDKl+lrY6weviaNnKw=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.39", "", { "os": "linux", "cpu": "x64" }, "sha512-xf9QdMC+qwQxtFAty/9RxgCLFdp9pFl09g86hxGPzlzCtHUjd+BmeUnUTXvVC8CHJLWECLQbFP6/233XHG0blA=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.39", "", { "os": "none", "cpu": "arm64" }, "sha512-QCvN02VpE6zFYry0zAU+29D5+O9tJELNt+OjuCubilZdD/S8xFdho7qBJaa3YhFYyA9cReOMVH8Z8b3yWb4hcA=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.39", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.5" }, "cpu": "none" }, "sha512-LFgshxApyBNiBHFVpun7tPrIQ4TvxW0f/endC5C4RzEHu7mxexBCQEkO5XrZ42Cr5DUY+ERNbkfNTUv+vVCaxQ=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.39", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mykirawg+s1e0uzVSEFhUBTShvXrOghPnyuLYkCfw8gzy8bMYiJuxsAfcopzZIIAVOHeSblJoiA/e7gYFjg8HA=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.39", "", { "os": "win32", "cpu": "ia32" }, "sha512-4PQJfWx7mdzXbAa4y+3OSSo911BZyJ/Is4pJKiwcGUqtvY66MX7BqlNWMr9QAozArAGE2knDubLqCQwZpK631w=="],
|
|
||||||
|
|
||||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.39", "", { "os": "win32", "cpu": "x64" }, "sha512-0zmmPOWbFfp1g9ofieimHwhuclZMcib0HL52Q+JTRpOHChI2f83TtH3duKWtAaxqhLUndTr/Z5sxzb+G2FNL9g=="],
|
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.39", "", {}, "sha512-GkTtNCV8ObWbq3LrJStPBv9jkRPct8WlwotVjx3aU0RwfH3LyheixWK9Zhaj22C4EQj/TJxYyetoX+uOn/MWKw=="],
|
|
||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
"@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="],
|
"@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="],
|
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
|
"@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="],
|
||||||
|
|
||||||
"@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="],
|
"@types/pegjs": ["@types/pegjs@0.10.6", "", {}, "sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
|
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
|
||||||
|
|
||||||
"ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="],
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
"bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="],
|
"big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="],
|
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
|
||||||
|
|
||||||
|
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
|
"discontinuous-range": ["discontinuous-range@1.0.0", "", {}, "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
|
||||||
|
|
||||||
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
|
||||||
|
|
||||||
"hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="],
|
"hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="],
|
||||||
|
|
||||||
"node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
|
"jsox": ["jsox@1.2.123", "", { "bin": { "jsox": "lib/cli.js" } }, "sha512-LYordXJ/0Q4G8pUE1Pvh4fkfGvZY7lRe4WIJKl0wr0rtFDVw9lcdNW95GH0DceJ6E9xh41zJNW0vreEz7xOxCw=="],
|
||||||
|
|
||||||
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
"moo": ["moo@0.5.2", "", {}, "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q=="],
|
||||||
|
|
||||||
|
"nearley": ["nearley@2.20.1", "", { "dependencies": { "commander": "^2.19.0", "moo": "^0.5.0", "railroad-diagrams": "^1.0.0", "randexp": "0.4.6" }, "bin": { "nearleyc": "bin/nearleyc.js", "nearley-test": "bin/nearley-test.js", "nearley-unparse": "bin/nearley-unparse.js", "nearley-railroad": "bin/nearley-railroad.js" } }, "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ=="],
|
||||||
|
|
||||||
|
"node-sql-parser": ["node-sql-parser@5.3.12", "", { "dependencies": { "@types/pegjs": "^0.10.0", "big-integer": "^1.6.48" } }, "sha512-GQBwA2e44qjbK0MzFwh5bNYefniV6cKT4KfjNDpuh/2EWipUEK1BCMc//moSidp8EF6fHB/EqJwUH9GCh9MAPg=="],
|
||||||
|
|
||||||
"oxc-parser": ["oxc-parser@0.74.0", "", { "dependencies": { "@oxc-project/types": "^0.74.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm64": "0.74.0", "@oxc-parser/binding-darwin-arm64": "0.74.0", "@oxc-parser/binding-darwin-x64": "0.74.0", "@oxc-parser/binding-freebsd-x64": "0.74.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.74.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.74.0", "@oxc-parser/binding-linux-arm64-gnu": "0.74.0", "@oxc-parser/binding-linux-arm64-musl": "0.74.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.74.0", "@oxc-parser/binding-linux-s390x-gnu": "0.74.0", "@oxc-parser/binding-linux-x64-gnu": "0.74.0", "@oxc-parser/binding-linux-x64-musl": "0.74.0", "@oxc-parser/binding-wasm32-wasi": "0.74.0", "@oxc-parser/binding-win32-arm64-msvc": "0.74.0", "@oxc-parser/binding-win32-x64-msvc": "0.74.0" } }, "sha512-2tDN/ttU8WE6oFh8EzKNam7KE7ZXSG5uXmvX85iNzxdJfMssDWcj3gpYzZi1E04XuE7m3v1dVWl/8BE886vPGw=="],
|
"oxc-parser": ["oxc-parser@0.74.0", "", { "dependencies": { "@oxc-project/types": "^0.74.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm64": "0.74.0", "@oxc-parser/binding-darwin-arm64": "0.74.0", "@oxc-parser/binding-darwin-x64": "0.74.0", "@oxc-parser/binding-freebsd-x64": "0.74.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.74.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.74.0", "@oxc-parser/binding-linux-arm64-gnu": "0.74.0", "@oxc-parser/binding-linux-arm64-musl": "0.74.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.74.0", "@oxc-parser/binding-linux-s390x-gnu": "0.74.0", "@oxc-parser/binding-linux-x64-gnu": "0.74.0", "@oxc-parser/binding-linux-x64-musl": "0.74.0", "@oxc-parser/binding-wasm32-wasi": "0.74.0", "@oxc-parser/binding-win32-arm64-msvc": "0.74.0", "@oxc-parser/binding-win32-x64-msvc": "0.74.0" } }, "sha512-2tDN/ttU8WE6oFh8EzKNam7KE7ZXSG5uXmvX85iNzxdJfMssDWcj3gpYzZi1E04XuE7m3v1dVWl/8BE886vPGw=="],
|
||||||
|
|
||||||
"oxlint": ["oxlint@1.16.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.16.0", "@oxlint/darwin-x64": "1.16.0", "@oxlint/linux-arm64-gnu": "1.16.0", "@oxlint/linux-arm64-musl": "1.16.0", "@oxlint/linux-x64-gnu": "1.16.0", "@oxlint/linux-x64-musl": "1.16.0", "@oxlint/win32-arm64": "1.16.0", "@oxlint/win32-x64": "1.16.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.2.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-o6z8s6QVw/d7QuxQ7QFfqDMrIcmHyU3J/MewxjqduJmy4vHt/s7OZISk8zEXjHXZzTWrcFakIrLqU/b9IKTcjg=="],
|
"oxlint": ["oxlint@1.19.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.19.0", "@oxlint/darwin-x64": "1.19.0", "@oxlint/linux-arm64-gnu": "1.19.0", "@oxlint/linux-arm64-musl": "1.19.0", "@oxlint/linux-x64-gnu": "1.19.0", "@oxlint/linux-x64-musl": "1.19.0", "@oxlint/win32-arm64": "1.19.0", "@oxlint/win32-x64": "1.19.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.2.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-MGeclRJFKaROXcPKMHOuJpOhbC4qkbLeZqSlelQioV/5YeBk/qVYZafUUpVO/yQ28Pld3srsTQusFtPNkVuvNA=="],
|
||||||
|
|
||||||
"pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="],
|
|
||||||
|
|
||||||
"pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
|
|
||||||
|
|
||||||
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
|
|
||||||
|
|
||||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
|
||||||
|
|
||||||
"pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
|
|
||||||
|
|
||||||
"pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
|
|
||||||
|
|
||||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
|
||||||
|
|
||||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
|
||||||
|
|
||||||
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
|
||||||
|
|
||||||
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
|
||||||
|
|
||||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
|
||||||
|
|
||||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
|
||||||
|
|
||||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"prettier-plugin-sql": ["prettier-plugin-sql@0.19.2", "", { "dependencies": { "jsox": "^1.2.123", "node-sql-parser": "^5.3.10", "sql-formatter": "^15.6.5", "tslib": "^2.8.1" }, "peerDependencies": { "prettier": "^3.0.3" } }, "sha512-DAu1Jcanpvs32OAOXsqaVXOpPs4nFLVkB3XwzRiZZVNL5/c+XdlNxWFMiMpMhYhmCG5BW3srK8mhikCOv5tPfg=="],
|
||||||
|
|
||||||
"rolldown": ["rolldown@1.0.0-beta.39", "", { "dependencies": { "@oxc-project/types": "=0.90.0", "@rolldown/pluginutils": "1.0.0-beta.39", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.39", "@rolldown/binding-darwin-arm64": "1.0.0-beta.39", "@rolldown/binding-darwin-x64": "1.0.0-beta.39", "@rolldown/binding-freebsd-x64": "1.0.0-beta.39", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.39", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.39", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.39", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.39", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.39", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.39", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.39", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.39", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.39", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.39" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-05bTT0CJU9dvCRC0Uc4zwB79W5N9MV9OG/Inyx8KNE2pSrrApJoWxEEArW6rmjx113HIx5IreCoTjzLfgvXTdg=="],
|
"railroad-diagrams": ["railroad-diagrams@1.0.0", "", {}, "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A=="],
|
||||||
|
|
||||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
"randexp": ["randexp@0.4.6", "", { "dependencies": { "discontinuous-range": "1.0.0", "ret": "~0.1.10" } }, "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ=="],
|
||||||
|
|
||||||
|
"ret": ["ret@0.1.15", "", {}, "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="],
|
||||||
|
|
||||||
|
"sql-formatter": ["sql-formatter@15.6.9", "", { "dependencies": { "argparse": "^2.0.1", "nearley": "^2.20.1" }, "bin": { "sql-formatter": "bin/sql-formatter-cli.cjs" } }, "sha512-r9VKnkRfKW7jbhTgytwbM+JqmFclQYN9L58Z3UTktuy9V1f1Y+rGK3t70Truh2wIOJzvZkzobAQ2PwGjjXsr6Q=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"tsx": ["tsx@4.20.5", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw=="],
|
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
|
"undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="],
|
||||||
|
|
||||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
|
||||||
|
|
||||||
"zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
"zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||||
|
|
||||||
"@oxc-parser/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
|
||||||
|
|
||||||
"oxc-parser/@oxc-project/types": ["@oxc-project/types@0.74.0", "", {}, "sha512-KOw/RZrVlHGhCXh1RufBFF7Nuo7HdY5w1lRJukM/igIl6x9qtz8QycDvZdzb4qnHO7znrPyo2sJrFJK2eKHgfQ=="],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
migrations/20240703103050_accounts.up.sql
Normal file
10
migrations/20240703103050_accounts.up.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS public.accounts (
|
||||||
|
id integer NOT NULL GENERATED ALWAYS AS IDENTITY,
|
||||||
|
username text NOT NULL,
|
||||||
|
password text NOT NULL,
|
||||||
|
role_id smallint NOT NULL DEFAULT '1',
|
||||||
|
created_at timestamp without time zone NOT NULL DEFAULT now (),
|
||||||
|
updated_at timestamp without time zone NOT NULL DEFAULT now (),
|
||||||
|
CONSTRAINT accounts_pkey PRIMARY KEY (id),
|
||||||
|
CONSTRAINT accounts_username_key UNIQUE (username)
|
||||||
|
);
|
||||||
11
migrations/20240703103105_movies.up.sql
Normal file
11
migrations/20240703103105_movies.up.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
CREATE TABLE IF NOT EXISTS public.movies (
|
||||||
|
id integer NOT NULL GENERATED ALWAYS AS IDENTITY,
|
||||||
|
title text NOT NULL,
|
||||||
|
overview text NOT NULL,
|
||||||
|
poster_path text NOT NULL,
|
||||||
|
backdrop_path text,
|
||||||
|
release_date date NOT NULL,
|
||||||
|
tmdb_id integer NOT NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
13
package.json
13
package.json
@@ -11,28 +11,25 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/app.js",
|
"main": "dist/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/app.ts",
|
"dev": "bun run --watch src/app.ts",
|
||||||
"lint": "oxlint .",
|
"lint": "oxlint .",
|
||||||
"type": "tsc --noEmit",
|
"type": "tsc --noEmit",
|
||||||
"build": "bun build --target=bun --outfile dist/app.js src/app.ts",
|
"build": "bun build --target=bun --outfile dist/app.js src/app.ts",
|
||||||
|
"build:migrate": "bun build --target=bun --outfile dist/migrate.js src/bin/migrate.ts",
|
||||||
"tests": "bun test"
|
"tests": "bun test"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@prettier/plugin-oxc": "^0.0.4",
|
"@prettier/plugin-oxc": "^0.0.4",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bun": "1.2.23",
|
||||||
"@types/node": "^24.2.1",
|
"@types/node": "^24.2.1",
|
||||||
"@types/pg": "^8.15.5",
|
"oxlint": "^1.19.0",
|
||||||
"@types/bun": "latest",
|
|
||||||
"oxlint": "^1.16.0",
|
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"tsx": "^4.20.5",
|
"prettier-plugin-sql": "^0.19.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/zod-validator": "^0.7.3",
|
"@hono/zod-validator": "^0.7.3",
|
||||||
"bcrypt": "^6.0.0",
|
|
||||||
"hono": "^4.9.8",
|
"hono": "^4.9.8",
|
||||||
"pg": "^8.16.3",
|
|
||||||
"zod": "^4.1.11"
|
"zod": "^4.1.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { setup as setupAccounts } from "./domain/account/setup";
|
|||||||
import { HTTPError } from "./errors";
|
import { HTTPError } from "./errors";
|
||||||
|
|
||||||
const config = loadConfiguration();
|
const config = loadConfiguration();
|
||||||
const database: DatabaseInterface = new PgDatabase(config.database);
|
const database: DatabaseInterface = PgDatabase.fromOptions(config.database);
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
app.route("/accounts", setupAccounts(database));
|
app.route("/accounts", setupAccounts(database));
|
||||||
|
|||||||
17
src/bin/migrate.ts
Normal file
17
src/bin/migrate.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import Migration from "../database/Migration";
|
||||||
|
import PgDatabase from "../database/PgDatabase";
|
||||||
|
|
||||||
|
const connectionString = process.env.DATABASE_URL;
|
||||||
|
if (!connectionString)
|
||||||
|
throw new Error("DATABASE_URL environment variable is not set");
|
||||||
|
|
||||||
|
const migrationsFolder = process.env.MIGRATIONS_FOLDER;
|
||||||
|
if (!migrationsFolder)
|
||||||
|
throw new Error("MIGRATIONS_FOLDER environment variable is not set");
|
||||||
|
|
||||||
|
const db = PgDatabase.fromConnectionString(connectionString);
|
||||||
|
await db.ping();
|
||||||
|
|
||||||
|
const migration = new Migration(db);
|
||||||
|
await migration.execute(path.resolve("migrations"));
|
||||||
@@ -24,6 +24,19 @@ export default function loadConfiguration(): Configuration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadTestConfiguration(): Configuration {
|
||||||
|
return {
|
||||||
|
database: {
|
||||||
|
host: getUnsafeEnv("TEST_DATABASE_HOST"),
|
||||||
|
port: parseInt(getUnsafeEnv("TEST_DATABASE_PORT")),
|
||||||
|
user: getUnsafeEnv("TEST_DATABASE_USER"),
|
||||||
|
password: getUnsafeEnv("TEST_DATABASE_PASSWORD"),
|
||||||
|
database: getUnsafeEnv("TEST_DATABASE_NAME"),
|
||||||
|
},
|
||||||
|
port: 3000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getEnvOrDefault(key: string, defaultValue: string): string {
|
export function getEnvOrDefault(key: string, defaultValue: string): string {
|
||||||
const value = process.env[key];
|
const value = process.env[key];
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export interface DatabaseInterface {
|
export interface DatabaseInterface {
|
||||||
ping(): Promise<void>;
|
ping(): Promise<void>;
|
||||||
fetchAll<T = any>(sql: string, params?: any[]): Promise<T[]>;
|
close(): Promise<void>;
|
||||||
fetchOne<T = any>(sql: string, params?: any[]): Promise<T | undefined>;
|
sql<T = any>(template: TemplateStringsArray, ...values: any[]): Promise<T>;
|
||||||
execute(sql: string, params?: any[]): Promise<void>;
|
exec(query: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/database/Migration.ts
Normal file
29
src/database/Migration.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Glob } from "bun";
|
||||||
|
import type { DatabaseInterface } from "./DatabaseInterface";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export default class Migration {
|
||||||
|
private readonly database: DatabaseInterface;
|
||||||
|
|
||||||
|
constructor(database: DatabaseInterface) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(folderPath: string) {
|
||||||
|
const files = await this.readFolder(folderPath);
|
||||||
|
const contents = await Promise.all(
|
||||||
|
files.map((file) => Bun.file(file).text()),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const content of contents) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.database.exec(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readFolder(folderPath: string) {
|
||||||
|
const glob = new Glob(path.join(folderPath, "*.sql"));
|
||||||
|
const files = await Array.fromAsync(glob.scan());
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Pool } from "pg";
|
import { SQL } from "bun";
|
||||||
import { DatabaseInterface } from "./DatabaseInterface";
|
import type { DatabaseInterface } from "./DatabaseInterface";
|
||||||
|
|
||||||
export interface PgDatabaseOptions {
|
export interface PgDatabaseOptions {
|
||||||
host: string;
|
host: string;
|
||||||
@@ -10,35 +10,38 @@ export interface PgDatabaseOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class PgDatabase implements DatabaseInterface {
|
export default class PgDatabase implements DatabaseInterface {
|
||||||
private readonly pool: Pool;
|
private readonly _sql: SQL;
|
||||||
|
|
||||||
constructor(options: PgDatabaseOptions) {
|
private constructor(connectionString: string) {
|
||||||
this.pool = new Pool({
|
this._sql = new SQL(connectionString);
|
||||||
host: options.host,
|
}
|
||||||
port: options.port,
|
|
||||||
user: options.user,
|
public static fromOptions(options: PgDatabaseOptions): PgDatabase {
|
||||||
password: options.password,
|
return new PgDatabase(
|
||||||
database: options.database,
|
`postgresql://${options.user}:${options.password}@${options.host}:${options.port}/${options.database}`,
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromConnectionString(connectionString: string): PgDatabase {
|
||||||
|
return new PgDatabase(connectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
await this._sql.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ping(): Promise<void> {
|
async ping(): Promise<void> {
|
||||||
await this.pool.query("SELECT 1");
|
await this._sql`SELECT 1`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchAll<T = any>(sql: string, params?: any[]): Promise<T[]> {
|
async sql<T = any>(
|
||||||
const res = await this.pool.query(sql, params);
|
template: TemplateStringsArray,
|
||||||
|
...values: any[]
|
||||||
return res.rows;
|
): Promise<T> {
|
||||||
|
return await this._sql(template, ...values);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchOne<T = any>(sql: string, params?: any[]): Promise<T | undefined> {
|
async exec(query: string): Promise<void> {
|
||||||
const res = await this.fetchAll<T>(sql, params);
|
await this._sql.unsafe(query);
|
||||||
|
|
||||||
return res[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(sql: string, params?: any[]): Promise<void> {
|
|
||||||
await this.pool.query(sql, params);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ export default function toRoutes(
|
|||||||
return parsed.data;
|
return parsed.data;
|
||||||
}),
|
}),
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const { email, password } = ctx.req.valid("json");
|
const { username, password } = ctx.req.valid("json");
|
||||||
const account = await accountService.login(email, password);
|
const account = await accountService.login(username, password);
|
||||||
|
|
||||||
return ctx.json({
|
return ctx.json({
|
||||||
success: true,
|
success: true,
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
email: account.email,
|
username: account.username,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -56,13 +56,13 @@ export default function toRoutes(
|
|||||||
return parsed.data;
|
return parsed.data;
|
||||||
}),
|
}),
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const { email, password } = ctx.req.valid("json");
|
const { username, password } = ctx.req.valid("json");
|
||||||
const account = await accountService.register(email, password);
|
const account = await accountService.register(username, password);
|
||||||
|
|
||||||
return ctx.json({
|
return ctx.json({
|
||||||
success: true,
|
success: true,
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
email: account.email,
|
username: account.username,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,30 +1,22 @@
|
|||||||
import bcrypt from "bcrypt";
|
|
||||||
import {
|
import {
|
||||||
InvalidEmailFormatError,
|
InvalidUsernameFormatError,
|
||||||
WeakPasswordError,
|
WeakPasswordError,
|
||||||
} from "../errors/AccountErrors";
|
} from "../errors/AccountErrors";
|
||||||
import {
|
import {
|
||||||
EMAIL_REGEX,
|
MAX_USERNAME_LENGTH,
|
||||||
MIN_PASSWORD_LENGTH,
|
MIN_PASSWORD_LENGTH,
|
||||||
|
MIN_USERNAME_LENGTH,
|
||||||
} from "../validation/AccountValidation";
|
} from "../validation/AccountValidation";
|
||||||
|
|
||||||
export class AccountEntity {
|
export class AccountEntity {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly id: string,
|
public readonly id: number,
|
||||||
public readonly email: string,
|
public readonly username: string,
|
||||||
private password: string,
|
private password: string,
|
||||||
public readonly roleId: number,
|
public readonly roleId: number,
|
||||||
public readonly createdAt: Date,
|
public readonly createdAt: Date,
|
||||||
public readonly updatedAt: Date,
|
public readonly updatedAt: Date,
|
||||||
) {
|
) {}
|
||||||
this.validateEmail(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateEmail(email: string): void {
|
|
||||||
if (!EMAIL_REGEX.test(email)) {
|
|
||||||
throw new InvalidEmailFormatError(email);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static validatePassword(password: string): void {
|
private static validatePassword(password: string): void {
|
||||||
if (password.length < MIN_PASSWORD_LENGTH) {
|
if (password.length < MIN_PASSWORD_LENGTH) {
|
||||||
@@ -32,26 +24,41 @@ export class AccountEntity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public verifyPassword(plainPassword: string): boolean {
|
private static validateUsername(username: string): void {
|
||||||
return bcrypt.compareSync(plainPassword, this.password);
|
if (
|
||||||
|
username.length < MIN_USERNAME_LENGTH ||
|
||||||
|
username.length > MAX_USERNAME_LENGTH
|
||||||
|
) {
|
||||||
|
throw new InvalidUsernameFormatError(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async verifyPassword(plainPassword: string): Promise<boolean> {
|
||||||
|
return await Bun.password.verify(plainPassword, this.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get hashedPassword(): string {
|
public get hashedPassword(): string {
|
||||||
return this.password;
|
return this.password;
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(email: string, password: string): AccountEntity {
|
static async create(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<CreateAccountEntity> {
|
||||||
AccountEntity.validatePassword(password);
|
AccountEntity.validatePassword(password);
|
||||||
|
AccountEntity.validateUsername(username);
|
||||||
|
|
||||||
const now = new Date();
|
const hashedPassword = await Bun.password.hash(password);
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const hashedPassword = bcrypt.hashSync(password, 10);
|
|
||||||
|
|
||||||
return new AccountEntity(id, email, hashedPassword, 1, now, now);
|
return {
|
||||||
|
username,
|
||||||
|
hashedPassword,
|
||||||
|
roleId: 1,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateAccountDto = {
|
export type CreateAccountEntity = Omit<
|
||||||
email: string;
|
AccountEntity,
|
||||||
password: string;
|
"id" | "verifyPassword" | "createdAt" | "updatedAt"
|
||||||
};
|
>;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ export class AccountNotFoundError extends HTTPError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AccountAlreadyExistsError extends HTTPError {
|
export class AccountAlreadyExistsError extends HTTPError {
|
||||||
constructor(email: string) {
|
constructor(username: string) {
|
||||||
super(409, `Un compte avec cet email existe déjà : ${email}`);
|
super(409, `Un compte avec cet username existe déjà : ${username}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,9 +19,9 @@ export class BadPasswordError extends HTTPError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InvalidEmailFormatError extends HTTPError {
|
export class InvalidUsernameFormatError extends HTTPError {
|
||||||
constructor(email: string) {
|
constructor(username: string) {
|
||||||
super(400, `Format d'email invalide : ${email}`);
|
super(400, `Format d'username invalide : ${username}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { AccountEntity } from "../entity/AccountEntity";
|
import {
|
||||||
|
AccountEntity,
|
||||||
|
type CreateAccountEntity,
|
||||||
|
} from "../entity/AccountEntity";
|
||||||
|
|
||||||
export interface AccountRepositoryInterface {
|
export interface AccountRepositoryInterface {
|
||||||
findByEmail(email: string): Promise<AccountEntity | null>;
|
findByUsername(username: string): Promise<AccountEntity | null>;
|
||||||
save(account: AccountEntity): Promise<string>;
|
insert(account: CreateAccountEntity): Promise<number>;
|
||||||
findById(id: string): Promise<AccountEntity | null>;
|
findById(id: number): Promise<AccountEntity | null>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
import type { DatabaseInterface } from "../../../database/DatabaseInterface";
|
import type { DatabaseInterface } from "../../../database/DatabaseInterface";
|
||||||
import { AccountEntity } from "../entity/AccountEntity";
|
import {
|
||||||
|
AccountEntity,
|
||||||
|
type CreateAccountEntity,
|
||||||
|
} from "../entity/AccountEntity";
|
||||||
import type { AccountRepositoryInterface } from "./AccountRepositoryInterface";
|
import type { AccountRepositoryInterface } from "./AccountRepositoryInterface";
|
||||||
|
|
||||||
export default class AccountRepository implements AccountRepositoryInterface {
|
export default class AccountRepository implements AccountRepositoryInterface {
|
||||||
constructor(private readonly database: DatabaseInterface) {}
|
constructor(private readonly database: DatabaseInterface) {}
|
||||||
|
|
||||||
async findByEmail(email: string): Promise<AccountEntity | null> {
|
async findByUsername(username: string): Promise<AccountEntity | null> {
|
||||||
const sql = `
|
const [result] = await this.database.sql<
|
||||||
SELECT id, email, password, role_id, created_at, updated_at
|
{
|
||||||
FROM accounts
|
id: number;
|
||||||
WHERE email = $1
|
username: string;
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await this.database.fetchOne<{
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
password: string;
|
||||||
role_id: number;
|
role_id: number;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
}>(sql, [email]);
|
}[]
|
||||||
|
>`
|
||||||
|
SELECT id, username, password, role_id, created_at, updated_at
|
||||||
|
FROM accounts
|
||||||
|
WHERE username = ${username}
|
||||||
|
`;
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return null;
|
return null;
|
||||||
@@ -27,7 +30,7 @@ export default class AccountRepository implements AccountRepositoryInterface {
|
|||||||
|
|
||||||
return new AccountEntity(
|
return new AccountEntity(
|
||||||
result.id,
|
result.id,
|
||||||
result.email,
|
result.username,
|
||||||
result.password,
|
result.password,
|
||||||
result.role_id,
|
result.role_id,
|
||||||
result.created_at,
|
result.created_at,
|
||||||
@@ -35,45 +38,31 @@ export default class AccountRepository implements AccountRepositoryInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(account: AccountEntity): Promise<string> {
|
async insert(account: CreateAccountEntity): Promise<number> {
|
||||||
const sql = `
|
const [result] = await this.database.sql<{ id: number }[]>`
|
||||||
INSERT INTO accounts (id, email, password, role_id, created_at, updated_at)
|
INSERT INTO accounts (username, password, role_id, created_at, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES (${account.username}, ${account.hashedPassword}, ${account.roleId}, NOW(), NOW())
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
|
||||||
email = EXCLUDED.email,
|
|
||||||
password = EXCLUDED.password,
|
|
||||||
role_id = EXCLUDED.role_id,
|
|
||||||
updated_at = EXCLUDED.updated_at
|
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await this.database.fetchOne<{ id: string }>(sql, [
|
|
||||||
account.id,
|
|
||||||
account.email,
|
|
||||||
account.hashedPassword,
|
|
||||||
account.roleId,
|
|
||||||
account.createdAt,
|
|
||||||
account.updatedAt,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return result!.id;
|
return result!.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string): Promise<AccountEntity | null> {
|
async findById(id: number): Promise<AccountEntity | null> {
|
||||||
const sql = `
|
const [result] = await this.database.sql<
|
||||||
SELECT id, email, password, role_id, created_at, updated_at
|
{
|
||||||
FROM accounts
|
id: number;
|
||||||
WHERE id = $1
|
username: string;
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await this.database.fetchOne<{
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
password: string;
|
||||||
role_id: number;
|
role_id: number;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
}>(sql, [id]);
|
}[]
|
||||||
|
>`
|
||||||
|
SELECT id, username, password, role_id, created_at, updated_at
|
||||||
|
FROM accounts
|
||||||
|
WHERE id = ${id}
|
||||||
|
`;
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return null;
|
return null;
|
||||||
@@ -81,7 +70,7 @@ export default class AccountRepository implements AccountRepositoryInterface {
|
|||||||
|
|
||||||
return new AccountEntity(
|
return new AccountEntity(
|
||||||
result.id,
|
result.id,
|
||||||
result.email,
|
result.username,
|
||||||
result.password,
|
result.password,
|
||||||
result.role_id,
|
result.role_id,
|
||||||
result.created_at,
|
result.created_at,
|
||||||
|
|||||||
@@ -10,30 +10,31 @@ import {
|
|||||||
export default class AccountService implements AccountServiceInterface {
|
export default class AccountService implements AccountServiceInterface {
|
||||||
constructor(private readonly accountRepository: AccountRepositoryInterface) {}
|
constructor(private readonly accountRepository: AccountRepositoryInterface) {}
|
||||||
|
|
||||||
async login(email: string, password: string): Promise<AccountEntity> {
|
async login(username: string, password: string): Promise<AccountEntity> {
|
||||||
const account = await this.accountRepository.findByEmail(email);
|
const account = await this.accountRepository.findByUsername(username);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new AccountNotFoundError(email);
|
throw new AccountNotFoundError(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!account.verifyPassword(password)) {
|
if (!(await account.verifyPassword(password))) {
|
||||||
throw new BadPasswordError();
|
throw new BadPasswordError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
async register(email: string, password: string): Promise<AccountEntity> {
|
async register(username: string, password: string): Promise<AccountEntity> {
|
||||||
const existingAccount = await this.accountRepository.findByEmail(email);
|
const existingAccount =
|
||||||
|
await this.accountRepository.findByUsername(username);
|
||||||
|
|
||||||
if (existingAccount) {
|
if (existingAccount) {
|
||||||
throw new AccountAlreadyExistsError(email);
|
throw new AccountAlreadyExistsError(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAccount = AccountEntity.create(email, password);
|
const newAccount = await AccountEntity.create(username, password);
|
||||||
await this.accountRepository.save(newAccount);
|
const newId = await this.accountRepository.insert(newAccount);
|
||||||
|
|
||||||
return newAccount;
|
return (await this.accountRepository.findById(newId)) as AccountEntity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
export const MIN_PASSWORD_LENGTH = 8;
|
export const MIN_PASSWORD_LENGTH = 8;
|
||||||
|
export const MAX_USERNAME_LENGTH = 20;
|
||||||
|
export const MIN_USERNAME_LENGTH = 3;
|
||||||
|
|
||||||
export const emailSchema = z
|
export const usernameSchema = z
|
||||||
.string()
|
.string()
|
||||||
.regex(EMAIL_REGEX, "Format d'email invalide");
|
.min(
|
||||||
|
MIN_USERNAME_LENGTH,
|
||||||
|
`Le nom d'utilisateur doit contenir au moins ${MIN_USERNAME_LENGTH} caractères`,
|
||||||
|
)
|
||||||
|
.max(
|
||||||
|
MAX_USERNAME_LENGTH,
|
||||||
|
`Le nom d'utilisateur doit contenir au plus ${MAX_USERNAME_LENGTH} caractères`,
|
||||||
|
);
|
||||||
|
|
||||||
export const passwordSchema = z
|
export const passwordSchema = z
|
||||||
.string()
|
.string()
|
||||||
@@ -15,11 +23,11 @@ export const passwordSchema = z
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const loginSchema = z.object({
|
export const loginSchema = z.object({
|
||||||
email: emailSchema,
|
username: usernameSchema,
|
||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const registerSchema = z.object({
|
export const registerSchema = z.object({
|
||||||
email: emailSchema,
|
username: usernameSchema,
|
||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Context, Next } from 'hono';
|
import type { Context, Next } from "hono";
|
||||||
import { TooManyAttemptsError } from '../domain/account/errors/AccountErrors';
|
import { TooManyAttemptsError } from "../domain/account/errors/AccountErrors";
|
||||||
|
|
||||||
interface RateLimitConfig {
|
interface RateLimitConfig {
|
||||||
windowMs: number;
|
windowMs: number;
|
||||||
@@ -24,7 +24,11 @@ function cleanupExpiredEntries(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createRateLimit(config: RateLimitConfig) {
|
export function createRateLimit(config: RateLimitConfig) {
|
||||||
const { windowMs, maxAttempts, keyGenerator = (c) => c.req.header('x-forwarded-for') || '127.0.0.1' } = config;
|
const {
|
||||||
|
windowMs,
|
||||||
|
maxAttempts,
|
||||||
|
keyGenerator = (c) => c.req.header("x-forwarded-for") || "127.0.0.1",
|
||||||
|
} = config;
|
||||||
|
|
||||||
return async (c: Context, next: Next) => {
|
return async (c: Context, next: Next) => {
|
||||||
const key = keyGenerator(c);
|
const key = keyGenerator(c);
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
|
||||||
import { AccountEntity } from "../src/domain/account/entity/AccountEntity";
|
|
||||||
import {
|
|
||||||
InvalidEmailFormatError,
|
|
||||||
WeakPasswordError,
|
|
||||||
} from "../src/domain/account/errors/AccountErrors";
|
|
||||||
import { MIN_PASSWORD_LENGTH } from "../src/domain/account/validation/AccountValidation";
|
|
||||||
|
|
||||||
describe("AccountEntity", () => {
|
|
||||||
describe("create", () => {
|
|
||||||
it("should create an account with valid email and password", () => {
|
|
||||||
const email = "test@example.com";
|
|
||||||
const password = "a".repeat(MIN_PASSWORD_LENGTH); // Use minimum length
|
|
||||||
|
|
||||||
const account = AccountEntity.create(email, password);
|
|
||||||
|
|
||||||
expect(account.email).toBe(email);
|
|
||||||
expect(account.roleId).toBe(1);
|
|
||||||
expect(account.id).toBeDefined();
|
|
||||||
expect(account.createdAt).toBeInstanceOf(Date);
|
|
||||||
expect(account.updatedAt).toBeInstanceOf(Date);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw InvalidEmailFormatError for invalid email", () => {
|
|
||||||
const invalidEmail = "invalid-email";
|
|
||||||
const password = "password123";
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
AccountEntity.create(invalidEmail, password);
|
|
||||||
}).toThrow(InvalidEmailFormatError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw WeakPasswordError for short password", () => {
|
|
||||||
const email = "test@example.com";
|
|
||||||
const shortPassword = "a".repeat(MIN_PASSWORD_LENGTH - 1); // One less than minimum
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
AccountEntity.create(email, shortPassword);
|
|
||||||
}).toThrow(WeakPasswordError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("verifyPassword", () => {
|
|
||||||
it("should return true for correct password", () => {
|
|
||||||
const account = AccountEntity.create("test@example.com", "password123");
|
|
||||||
|
|
||||||
expect(account.verifyPassword("password123")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false for incorrect password", () => {
|
|
||||||
const account = AccountEntity.create("test@example.com", "password123");
|
|
||||||
|
|
||||||
expect(account.verifyPassword("wrongpassword")).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { describe, it, expect, jest, beforeEach } from "bun:test";
|
|
||||||
import AccountRepository from "../src/domain/account/repository/AccoutRepository";
|
|
||||||
import { AccountEntity } from "../src/domain/account/entity/AccountEntity";
|
|
||||||
|
|
||||||
describe("AccountRepository", () => {
|
|
||||||
let accountRepository: AccountRepository;
|
|
||||||
let mockDatabase: {
|
|
||||||
ping: ReturnType<typeof jest.fn>;
|
|
||||||
fetchAll: ReturnType<typeof jest.fn>;
|
|
||||||
fetchOne: ReturnType<typeof jest.fn>;
|
|
||||||
execute: ReturnType<typeof jest.fn>;
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockDatabase = {
|
|
||||||
ping: jest.fn(() => Promise.resolve()),
|
|
||||||
fetchAll: jest.fn(() => Promise.resolve([])),
|
|
||||||
fetchOne: jest.fn(() => Promise.resolve(undefined)),
|
|
||||||
execute: jest.fn(() => Promise.resolve()),
|
|
||||||
};
|
|
||||||
accountRepository = new AccountRepository(mockDatabase as any);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findByEmail", () => {
|
|
||||||
it("should return account when found", async () => {
|
|
||||||
const email = "test@example.com";
|
|
||||||
const mockResult = {
|
|
||||||
id: "123",
|
|
||||||
email: email,
|
|
||||||
password: "hashedPassword",
|
|
||||||
role_id: 1,
|
|
||||||
created_at: new Date(),
|
|
||||||
updated_at: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockDatabase.fetchOne.mockImplementation(() =>
|
|
||||||
Promise.resolve(mockResult),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await accountRepository.findByEmail(email);
|
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(AccountEntity);
|
|
||||||
expect(result?.email).toBe(email);
|
|
||||||
expect(mockDatabase.fetchOne).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining(
|
|
||||||
"SELECT id, email, password, role_id, created_at, updated_at",
|
|
||||||
),
|
|
||||||
[email],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return null when account not found", async () => {
|
|
||||||
const email = "nonexistent@example.com";
|
|
||||||
|
|
||||||
mockDatabase.fetchOne.mockImplementation(() =>
|
|
||||||
Promise.resolve(undefined),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await accountRepository.findByEmail(email);
|
|
||||||
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findById", () => {
|
|
||||||
it("should return account when found", async () => {
|
|
||||||
const id = "123";
|
|
||||||
const mockResult = {
|
|
||||||
id: id,
|
|
||||||
email: "test@example.com",
|
|
||||||
password: "hashedPassword",
|
|
||||||
role_id: 1,
|
|
||||||
created_at: new Date(),
|
|
||||||
updated_at: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockDatabase.fetchOne.mockImplementation(() =>
|
|
||||||
Promise.resolve(mockResult),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await accountRepository.findById(id);
|
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(AccountEntity);
|
|
||||||
expect(result?.id).toBe(id);
|
|
||||||
expect(mockDatabase.fetchOne).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining(
|
|
||||||
"SELECT id, email, password, role_id, created_at, updated_at",
|
|
||||||
),
|
|
||||||
[id],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return null when account not found", async () => {
|
|
||||||
const id = "nonexistent";
|
|
||||||
|
|
||||||
mockDatabase.fetchOne.mockImplementation(() =>
|
|
||||||
Promise.resolve(undefined),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await accountRepository.findById(id);
|
|
||||||
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("save", () => {
|
|
||||||
it("should save account successfully", async () => {
|
|
||||||
const account = AccountEntity.create("test@example.com", "password123");
|
|
||||||
|
|
||||||
mockDatabase.fetchOne.mockImplementation(() =>
|
|
||||||
Promise.resolve({ id: account.id }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await accountRepository.save(account);
|
|
||||||
|
|
||||||
expect(result).toBe(account.id);
|
|
||||||
expect(mockDatabase.fetchOne).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining("INSERT INTO accounts"),
|
|
||||||
expect.arrayContaining([
|
|
||||||
account.id,
|
|
||||||
account.email,
|
|
||||||
expect.any(String),
|
|
||||||
account.roleId,
|
|
||||||
account.createdAt,
|
|
||||||
account.updatedAt,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import { describe, it, expect, jest, beforeEach } from "bun:test";
|
|
||||||
import AccountService from "../src/domain/account/service/AccountService";
|
|
||||||
import { AccountEntity } from "../src/domain/account/entity/AccountEntity";
|
|
||||||
import {
|
|
||||||
AccountNotFoundError,
|
|
||||||
AccountAlreadyExistsError,
|
|
||||||
BadPasswordError,
|
|
||||||
} from "../src/domain/account/errors/AccountErrors";
|
|
||||||
|
|
||||||
describe("AccountService", () => {
|
|
||||||
let accountService: AccountService;
|
|
||||||
let mockAccountRepository: {
|
|
||||||
findByEmail: ReturnType<typeof jest.fn>;
|
|
||||||
save: ReturnType<typeof jest.fn>;
|
|
||||||
findById: ReturnType<typeof jest.fn>;
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockAccountRepository = {
|
|
||||||
findByEmail: jest.fn(() => Promise.resolve(null)),
|
|
||||||
save: jest.fn(() => Promise.resolve("123")),
|
|
||||||
findById: jest.fn(() => Promise.resolve(null)),
|
|
||||||
};
|
|
||||||
accountService = new AccountService(mockAccountRepository as any);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("createAccount", () => {
|
|
||||||
it("should create a new account successfully", async () => {
|
|
||||||
const email = "test@example.com";
|
|
||||||
const password = "password123";
|
|
||||||
|
|
||||||
mockAccountRepository.findByEmail.mockImplementation(() =>
|
|
||||||
Promise.resolve(null),
|
|
||||||
);
|
|
||||||
mockAccountRepository.save.mockImplementation(() =>
|
|
||||||
Promise.resolve("123"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await accountService.register(email, password);
|
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(AccountEntity);
|
|
||||||
expect(result.email).toBe(email);
|
|
||||||
expect(mockAccountRepository.findByEmail).toHaveBeenCalledWith(email);
|
|
||||||
expect(mockAccountRepository.save).toHaveBeenCalledWith(
|
|
||||||
expect.any(AccountEntity),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw error if account already exists", async () => {
|
|
||||||
const email = "test@example.com";
|
|
||||||
const password = "password123";
|
|
||||||
const existingAccount = AccountEntity.create(email, password);
|
|
||||||
|
|
||||||
mockAccountRepository.findByEmail.mockImplementation(() =>
|
|
||||||
Promise.resolve(existingAccount),
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(accountService.register(email, password)).rejects.toThrow(
|
|
||||||
AccountAlreadyExistsError,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("login", () => {
|
|
||||||
it("should login successfully with correct credentials", async () => {
|
|
||||||
const email = "test@example.com";
|
|
||||||
const password = "password123";
|
|
||||||
const account = AccountEntity.create(email, password);
|
|
||||||
|
|
||||||
mockAccountRepository.findByEmail.mockImplementation(() =>
|
|
||||||
Promise.resolve(account),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await accountService.login(email, password);
|
|
||||||
|
|
||||||
expect(result).toBe(account);
|
|
||||||
expect(mockAccountRepository.findByEmail).toHaveBeenCalledWith(email);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw error if account not found", async () => {
|
|
||||||
const email = "test@example.com";
|
|
||||||
const password = "password123";
|
|
||||||
|
|
||||||
mockAccountRepository.findByEmail.mockImplementation(() =>
|
|
||||||
Promise.resolve(null),
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(accountService.login(email, password)).rejects.toThrow(
|
|
||||||
AccountNotFoundError,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw error if password is incorrect", async () => {
|
|
||||||
const email = "test@example.com";
|
|
||||||
const password = "password123";
|
|
||||||
const wrongPassword = "wrongpassword";
|
|
||||||
const account = AccountEntity.create(email, password);
|
|
||||||
|
|
||||||
mockAccountRepository.findByEmail.mockImplementation(() =>
|
|
||||||
Promise.resolve(account),
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(accountService.login(email, wrongPassword)).rejects.toThrow(
|
|
||||||
BadPasswordError,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
80
tests/account/AccountEntity.test.ts
Normal file
80
tests/account/AccountEntity.test.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import {
|
||||||
|
InvalidUsernameFormatError,
|
||||||
|
WeakPasswordError,
|
||||||
|
} from "../../src/domain/account/errors/AccountErrors";
|
||||||
|
import {
|
||||||
|
MAX_USERNAME_LENGTH,
|
||||||
|
MIN_PASSWORD_LENGTH,
|
||||||
|
} from "../../src/domain/account/validation/AccountValidation";
|
||||||
|
import { AccountEntity } from "../../src/domain/account/entity/AccountEntity";
|
||||||
|
|
||||||
|
describe("AccountEntity", () => {
|
||||||
|
describe("create", () => {
|
||||||
|
it("should create an account with valid username and password", async () => {
|
||||||
|
const email = "testaccount";
|
||||||
|
const password = "a".repeat(MIN_PASSWORD_LENGTH);
|
||||||
|
|
||||||
|
const account = await AccountEntity.create(email, password);
|
||||||
|
|
||||||
|
expect(account.username).toBe(email);
|
||||||
|
expect(account.roleId).toBe(1);
|
||||||
|
expect(account).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw InvalidUsernameFormatError for invalid username", () => {
|
||||||
|
const invalidUsername = "a".repeat(MAX_USERNAME_LENGTH + 1);
|
||||||
|
const password = "a".repeat(MIN_PASSWORD_LENGTH);
|
||||||
|
|
||||||
|
expect(AccountEntity.create(invalidUsername, password)).rejects.toThrow(
|
||||||
|
InvalidUsernameFormatError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw WeakPasswordError for short password", () => {
|
||||||
|
const username = "testaccount";
|
||||||
|
const shortPassword = "a".repeat(MIN_PASSWORD_LENGTH - 1);
|
||||||
|
|
||||||
|
expect(AccountEntity.create(username, shortPassword)).rejects.toThrow(
|
||||||
|
WeakPasswordError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("verifyPassword", () => {
|
||||||
|
it("should return true for correct password", async () => {
|
||||||
|
const createAccount = await AccountEntity.create(
|
||||||
|
"test@example.com",
|
||||||
|
"password123",
|
||||||
|
);
|
||||||
|
|
||||||
|
const account = new AccountEntity(
|
||||||
|
1,
|
||||||
|
createAccount.username,
|
||||||
|
createAccount.hashedPassword,
|
||||||
|
createAccount.roleId,
|
||||||
|
new Date(),
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await account.verifyPassword("password123")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for incorrect password", async () => {
|
||||||
|
const createAccount = await AccountEntity.create(
|
||||||
|
"test@example.com",
|
||||||
|
"password123",
|
||||||
|
);
|
||||||
|
const account = new AccountEntity(
|
||||||
|
1,
|
||||||
|
createAccount.username,
|
||||||
|
createAccount.hashedPassword,
|
||||||
|
createAccount.roleId,
|
||||||
|
new Date(),
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await account.verifyPassword("wrongpassword")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
74
tests/account/AccountRepository.test.ts
Normal file
74
tests/account/AccountRepository.test.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||||
|
import AccountRepository from "../../src/domain/account/repository/AccoutRepository";
|
||||||
|
import { AccountEntity } from "../../src/domain/account/entity/AccountEntity";
|
||||||
|
import { initTestDatabase } from "../utils";
|
||||||
|
import type { DatabaseInterface } from "../../src/database/DatabaseInterface";
|
||||||
|
|
||||||
|
describe("AccountRepository", () => {
|
||||||
|
let accountRepository: AccountRepository;
|
||||||
|
let database: DatabaseInterface;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
database = await initTestDatabase();
|
||||||
|
accountRepository = new AccountRepository(database);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await database?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findByUsername", () => {
|
||||||
|
it("should return account when found", async () => {
|
||||||
|
const username = "testaccount";
|
||||||
|
|
||||||
|
const createAccount = await AccountEntity.create(username, "password123");
|
||||||
|
await accountRepository.insert(createAccount);
|
||||||
|
|
||||||
|
const result = await accountRepository.findByUsername(username);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(AccountEntity);
|
||||||
|
expect(result?.username).toBe(username);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when account not found", async () => {
|
||||||
|
const username = "nonexistentaccount";
|
||||||
|
const result = await accountRepository.findByUsername(username);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findById", () => {
|
||||||
|
it("should return account when found", async () => {
|
||||||
|
const createAccount = await AccountEntity.create(
|
||||||
|
"test@example.com",
|
||||||
|
"password123",
|
||||||
|
);
|
||||||
|
|
||||||
|
const newId = await accountRepository.insert(createAccount);
|
||||||
|
const result = await accountRepository.findById(newId);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(AccountEntity);
|
||||||
|
expect(result?.id).toBe(newId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when account not found", async () => {
|
||||||
|
const id = 999;
|
||||||
|
const result = await accountRepository.findById(id);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("insert", () => {
|
||||||
|
it("should insert account successfully", async () => {
|
||||||
|
const account = await AccountEntity.create(
|
||||||
|
"test@example.com",
|
||||||
|
"password123",
|
||||||
|
);
|
||||||
|
const result = await accountRepository.insert(account);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
145
tests/account/AccountService.test.ts
Normal file
145
tests/account/AccountService.test.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { describe, it, expect, jest, beforeEach } from "bun:test";
|
||||||
|
import {
|
||||||
|
AccountNotFoundError,
|
||||||
|
AccountAlreadyExistsError,
|
||||||
|
BadPasswordError,
|
||||||
|
} from "../../src/domain/account/errors/AccountErrors";
|
||||||
|
import AccountService from "../../src/domain/account/service/AccountService";
|
||||||
|
import { AccountEntity } from "../../src/domain/account/entity/AccountEntity";
|
||||||
|
|
||||||
|
describe("AccountService", () => {
|
||||||
|
let accountService: AccountService;
|
||||||
|
let mockAccountRepository: {
|
||||||
|
findByUsername: ReturnType<typeof jest.fn>;
|
||||||
|
insert: ReturnType<typeof jest.fn>;
|
||||||
|
findById: ReturnType<typeof jest.fn>;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockAccountRepository = {
|
||||||
|
findByUsername: jest.fn(() => Promise.resolve(null)),
|
||||||
|
insert: jest.fn(() => Promise.resolve("123")),
|
||||||
|
findById: jest.fn(() => Promise.resolve(null)),
|
||||||
|
};
|
||||||
|
accountService = new AccountService(mockAccountRepository as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createAccount", () => {
|
||||||
|
it("should create a new account successfully", async () => {
|
||||||
|
const username = "testusername";
|
||||||
|
const password = "password123";
|
||||||
|
|
||||||
|
mockAccountRepository.findByUsername.mockImplementation(() =>
|
||||||
|
Promise.resolve(null),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockAccountRepository.findById.mockImplementation(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
new AccountEntity(
|
||||||
|
123,
|
||||||
|
username,
|
||||||
|
"hashedpassword",
|
||||||
|
1,
|
||||||
|
new Date(),
|
||||||
|
new Date(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockAccountRepository.insert.mockImplementation(() =>
|
||||||
|
Promise.resolve(123),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await accountService.register(username, password);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(AccountEntity);
|
||||||
|
expect(result.username).toBe(username);
|
||||||
|
expect(mockAccountRepository.findByUsername).toHaveBeenCalledWith(
|
||||||
|
username,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if account already exists", async () => {
|
||||||
|
const username = "testusername";
|
||||||
|
const password = "password123";
|
||||||
|
const existingAccount = await AccountEntity.create(username, password);
|
||||||
|
|
||||||
|
mockAccountRepository.findByUsername.mockImplementation(() =>
|
||||||
|
Promise.resolve(existingAccount),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(accountService.register(username, password)).rejects.toThrow(
|
||||||
|
AccountAlreadyExistsError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("login", () => {
|
||||||
|
it("should login successfully with correct credentials", async () => {
|
||||||
|
const username = "testaccount";
|
||||||
|
const password = "password123";
|
||||||
|
|
||||||
|
const createAccount = await AccountEntity.create(username, password);
|
||||||
|
const account = new AccountEntity(
|
||||||
|
1,
|
||||||
|
createAccount.username,
|
||||||
|
createAccount.hashedPassword,
|
||||||
|
createAccount.roleId,
|
||||||
|
new Date(),
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockAccountRepository.findByUsername.mockImplementation(() =>
|
||||||
|
Promise.resolve(account),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await accountService.login(username, password);
|
||||||
|
|
||||||
|
expect({ username: result.username, roleId: result.roleId }).toEqual({
|
||||||
|
username: account.username,
|
||||||
|
roleId: account.roleId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAccountRepository.findByUsername).toHaveBeenCalledWith(
|
||||||
|
username,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if account not found", async () => {
|
||||||
|
const username = "testaccount";
|
||||||
|
const password = "password123";
|
||||||
|
|
||||||
|
mockAccountRepository.findByUsername.mockImplementation(() =>
|
||||||
|
Promise.resolve(null),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(accountService.login(username, password)).rejects.toThrow(
|
||||||
|
AccountNotFoundError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if password is incorrect", async () => {
|
||||||
|
const username = "testaccount";
|
||||||
|
const password = "password123";
|
||||||
|
const wrongPassword = "wrongpassword";
|
||||||
|
|
||||||
|
const createAccount = await AccountEntity.create(username, password);
|
||||||
|
const account = new AccountEntity(
|
||||||
|
1,
|
||||||
|
createAccount.username,
|
||||||
|
createAccount.hashedPassword,
|
||||||
|
createAccount.roleId,
|
||||||
|
new Date(),
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockAccountRepository.findByUsername.mockImplementation(() =>
|
||||||
|
Promise.resolve(account),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(accountService.login(username, wrongPassword)).rejects.toThrow(
|
||||||
|
BadPasswordError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,62 +1,14 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
import { describe, it, expect } from "bun:test";
|
||||||
import {
|
import {
|
||||||
emailSchema,
|
|
||||||
passwordSchema,
|
passwordSchema,
|
||||||
loginSchema,
|
loginSchema,
|
||||||
registerSchema,
|
registerSchema,
|
||||||
EMAIL_REGEX,
|
|
||||||
MIN_PASSWORD_LENGTH,
|
MIN_PASSWORD_LENGTH,
|
||||||
} from "../src/domain/account/validation/AccountValidation";
|
} from "../../src/domain/account/validation/AccountValidation";
|
||||||
import { AccountEntity } from "../src/domain/account/entity/AccountEntity";
|
import { WeakPasswordError } from "../../src/domain/account/errors/AccountErrors";
|
||||||
import {
|
import { AccountEntity } from "../../src/domain/account/entity/AccountEntity";
|
||||||
InvalidEmailFormatError,
|
|
||||||
WeakPasswordError,
|
|
||||||
} from "../src/domain/account/errors/AccountErrors";
|
|
||||||
|
|
||||||
describe("AccountValidation", () => {
|
describe("AccountValidation", () => {
|
||||||
describe("Email validation consistency", () => {
|
|
||||||
const validEmails = [
|
|
||||||
"test@example.com",
|
|
||||||
"user.name@domain.co.uk",
|
|
||||||
"firstname+lastname@example.org",
|
|
||||||
];
|
|
||||||
|
|
||||||
const invalidEmails = [
|
|
||||||
"invalid-email",
|
|
||||||
"@example.com",
|
|
||||||
"test@",
|
|
||||||
"test",
|
|
||||||
"test@domain",
|
|
||||||
"",
|
|
||||||
];
|
|
||||||
|
|
||||||
it("should validate same emails in Zod and Entity", () => {
|
|
||||||
validEmails.forEach((email) => {
|
|
||||||
expect(emailSchema.safeParse(email).success).toBe(true);
|
|
||||||
expect(() => AccountEntity.create(email, "password123")).not.toThrow(
|
|
||||||
InvalidEmailFormatError,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
invalidEmails.forEach((email) => {
|
|
||||||
expect(emailSchema.safeParse(email).success).toBe(false);
|
|
||||||
expect(() => AccountEntity.create(email, "password123")).toThrow(
|
|
||||||
InvalidEmailFormatError,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should use same regex pattern", () => {
|
|
||||||
validEmails.forEach((email) => {
|
|
||||||
expect(EMAIL_REGEX.test(email)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
invalidEmails.forEach((email) => {
|
|
||||||
expect(EMAIL_REGEX.test(email)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Password validation consistency", () => {
|
describe("Password validation consistency", () => {
|
||||||
const validPasswords = ["password123", "abcdefgh", "12345678", "P@ssw0rd!"];
|
const validPasswords = ["password123", "abcdefgh", "12345678", "P@ssw0rd!"];
|
||||||
|
|
||||||
@@ -91,8 +43,11 @@ describe("AccountValidation", () => {
|
|||||||
|
|
||||||
describe("Complete schemas", () => {
|
describe("Complete schemas", () => {
|
||||||
it("should validate login schema correctly", () => {
|
it("should validate login schema correctly", () => {
|
||||||
const validLogin = { email: "test@example.com", password: "password123" };
|
const validLogin = {
|
||||||
const invalidLogin = { email: "invalid", password: "123" };
|
username: "testaccount",
|
||||||
|
password: "password123",
|
||||||
|
};
|
||||||
|
const invalidLogin = { username: "ii", password: "123" };
|
||||||
|
|
||||||
expect(loginSchema.safeParse(validLogin).success).toBe(true);
|
expect(loginSchema.safeParse(validLogin).success).toBe(true);
|
||||||
expect(loginSchema.safeParse(invalidLogin).success).toBe(false);
|
expect(loginSchema.safeParse(invalidLogin).success).toBe(false);
|
||||||
@@ -100,10 +55,10 @@ describe("AccountValidation", () => {
|
|||||||
|
|
||||||
it("should validate register schema correctly", () => {
|
it("should validate register schema correctly", () => {
|
||||||
const validRegister = {
|
const validRegister = {
|
||||||
email: "test@example.com",
|
username: "testaccount",
|
||||||
password: "password123",
|
password: "password123",
|
||||||
};
|
};
|
||||||
const invalidRegister = { email: "invalid", password: "123" };
|
const invalidRegister = { username: "ii", password: "123" };
|
||||||
|
|
||||||
expect(registerSchema.safeParse(validRegister).success).toBe(true);
|
expect(registerSchema.safeParse(validRegister).success).toBe(true);
|
||||||
expect(registerSchema.safeParse(invalidRegister).success).toBe(false);
|
expect(registerSchema.safeParse(invalidRegister).success).toBe(false);
|
||||||
@@ -121,11 +76,17 @@ describe("AccountValidation", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return consistent email error message", () => {
|
it("should return consistent username error message", () => {
|
||||||
const result = emailSchema.safeParse("invalid-email");
|
const result = registerSchema.safeParse({
|
||||||
|
username: "ii",
|
||||||
|
password: "validPassword123",
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
expect(result.error.issues[0]?.message).toBe("Format d'email invalide");
|
expect(result.error.issues[0]?.message).toContain(
|
||||||
|
"doit contenir au moins",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
20
tests/utils.ts
Normal file
20
tests/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { loadTestConfiguration } from "../src/config";
|
||||||
|
import Migration from "../src/database/Migration";
|
||||||
|
import PgDatabase from "../src/database/PgDatabase";
|
||||||
|
import type { DatabaseInterface } from "../src/database/DatabaseInterface";
|
||||||
|
|
||||||
|
export async function initTestDatabase(): Promise<DatabaseInterface> {
|
||||||
|
const testConfiguration = loadTestConfiguration();
|
||||||
|
|
||||||
|
const database = PgDatabase.fromOptions(testConfiguration.database);
|
||||||
|
await database.ping();
|
||||||
|
|
||||||
|
await database.exec("DROP SCHEMA public CASCADE");
|
||||||
|
await database.exec("CREATE SCHEMA public");
|
||||||
|
|
||||||
|
const migration = new Migration(database);
|
||||||
|
await migration.execute(path.resolve("migrations"));
|
||||||
|
|
||||||
|
return database;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user