diff --git a/.prettierrc b/.prettierrc index 30a4169..3fc2ee7 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,3 @@ { - "plugins": ["@prettier/plugin-oxc"] + "plugins": ["@prettier/plugin-oxc", "prettier-plugin-sql"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b842b9..0143925 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,4 +3,4 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "typescript.tsdk": "node_modules/typescript/lib" -} +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index 1545735..0a007b1 100644 --- a/bun.lock +++ b/bun.lock @@ -5,21 +5,16 @@ "name": "nixi-api", "dependencies": { "@hono/zod-validator": "^0.7.3", - "bcrypt": "^6.0.0", "hono": "^4.9.8", - "pg": "^8.16.3", "zod": "^4.1.11", }, "devDependencies": { "@prettier/plugin-oxc": "^0.0.4", - "@types/bcrypt": "^6.0.0", - "@types/bun": "latest", + "@types/bun": "1.2.23", "@types/node": "^24.2.1", - "@types/pg": "^8.15.5", - "oxlint": "^1.16.0", + "oxlint": "^1.19.0", "prettier": "3.6.2", - "rolldown": "^1.0.0-beta.39", - "tsx": "^4.20.5", + "prettier-plugin-sql": "^0.19.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=="], - "@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=="], - "@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=="], @@ -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-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=="], - "@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=="], - "@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.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + "@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="], "@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=="], - "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=="], - "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=="], - - "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=="], + "discontinuous-range": ["discontinuous-range@1.0.0", "", {}, "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ=="], "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=="], - "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=="], - - "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=="], + "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=="], "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=="], - "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=="], "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=="], - - "@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=="], } } diff --git a/migrations/20240703103050_accounts.up.sql b/migrations/20240703103050_accounts.up.sql new file mode 100644 index 0000000..2c4905c --- /dev/null +++ b/migrations/20240703103050_accounts.up.sql @@ -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) +); diff --git a/migrations/20240703103105_movies.up.sql b/migrations/20240703103105_movies.up.sql new file mode 100644 index 0000000..7995863 --- /dev/null +++ b/migrations/20240703103105_movies.up.sql @@ -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) +); diff --git a/package.json b/package.json index cc65dda..8f1fec3 100644 --- a/package.json +++ b/package.json @@ -11,28 +11,25 @@ "type": "module", "main": "dist/app.js", "scripts": { - "dev": "tsx watch src/app.ts", + "dev": "bun run --watch src/app.ts", "lint": "oxlint .", "type": "tsc --noEmit", "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" }, "devDependencies": { "@prettier/plugin-oxc": "^0.0.4", - "@types/bcrypt": "^6.0.0", + "@types/bun": "1.2.23", "@types/node": "^24.2.1", - "@types/pg": "^8.15.5", - "@types/bun": "latest", - "oxlint": "^1.16.0", + "oxlint": "^1.19.0", "prettier": "3.6.2", - "tsx": "^4.20.5", + "prettier-plugin-sql": "^0.19.2", "typescript": "^5.9.2" }, "dependencies": { "@hono/zod-validator": "^0.7.3", - "bcrypt": "^6.0.0", "hono": "^4.9.8", - "pg": "^8.16.3", "zod": "^4.1.11" } } diff --git a/src/app.ts b/src/app.ts index 6a2d8a6..869adf3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,7 +6,7 @@ import { setup as setupAccounts } from "./domain/account/setup"; import { HTTPError } from "./errors"; const config = loadConfiguration(); -const database: DatabaseInterface = new PgDatabase(config.database); +const database: DatabaseInterface = PgDatabase.fromOptions(config.database); const app = new Hono(); app.route("/accounts", setupAccounts(database)); diff --git a/src/bin/migrate.ts b/src/bin/migrate.ts new file mode 100644 index 0000000..eab5fc4 --- /dev/null +++ b/src/bin/migrate.ts @@ -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")); diff --git a/src/config.ts b/src/config.ts index 86ffaf5..bbd1e3b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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 { const value = process.env[key]; if (value === undefined) { diff --git a/src/database/DatabaseInterface.ts b/src/database/DatabaseInterface.ts index cede7d1..4a49973 100644 --- a/src/database/DatabaseInterface.ts +++ b/src/database/DatabaseInterface.ts @@ -1,6 +1,6 @@ export interface DatabaseInterface { ping(): Promise; - fetchAll(sql: string, params?: any[]): Promise; - fetchOne(sql: string, params?: any[]): Promise; - execute(sql: string, params?: any[]): Promise; + close(): Promise; + sql(template: TemplateStringsArray, ...values: any[]): Promise; + exec(query: string): Promise; } diff --git a/src/database/Migration.ts b/src/database/Migration.ts new file mode 100644 index 0000000..78626ad --- /dev/null +++ b/src/database/Migration.ts @@ -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; + } +} diff --git a/src/database/PgDatabase.ts b/src/database/PgDatabase.ts index 5e25497..06dae02 100644 --- a/src/database/PgDatabase.ts +++ b/src/database/PgDatabase.ts @@ -1,5 +1,5 @@ -import { Pool } from "pg"; -import { DatabaseInterface } from "./DatabaseInterface"; +import { SQL } from "bun"; +import type { DatabaseInterface } from "./DatabaseInterface"; export interface PgDatabaseOptions { host: string; @@ -10,35 +10,38 @@ export interface PgDatabaseOptions { } export default class PgDatabase implements DatabaseInterface { - private readonly pool: Pool; + private readonly _sql: SQL; - constructor(options: PgDatabaseOptions) { - this.pool = new Pool({ - host: options.host, - port: options.port, - user: options.user, - password: options.password, - database: options.database, - }); + private constructor(connectionString: string) { + this._sql = new SQL(connectionString); + } + + public static fromOptions(options: PgDatabaseOptions): PgDatabase { + return new PgDatabase( + `postgresql://${options.user}:${options.password}@${options.host}:${options.port}/${options.database}`, + ); + } + + public static fromConnectionString(connectionString: string): PgDatabase { + return new PgDatabase(connectionString); + } + + async close(): Promise { + await this._sql.close(); } async ping(): Promise { - await this.pool.query("SELECT 1"); + await this._sql`SELECT 1`; } - async fetchAll(sql: string, params?: any[]): Promise { - const res = await this.pool.query(sql, params); - - return res.rows; + async sql( + template: TemplateStringsArray, + ...values: any[] + ): Promise { + return await this._sql(template, ...values); } - async fetchOne(sql: string, params?: any[]): Promise { - const res = await this.fetchAll(sql, params); - - return res[0]; - } - - async execute(sql: string, params?: any[]): Promise { - await this.pool.query(sql, params); + async exec(query: string): Promise { + await this._sql.unsafe(query); } } diff --git a/src/domain/account/controller/AccountController.ts b/src/domain/account/controller/AccountController.ts index 4dd1489..97eb042 100644 --- a/src/domain/account/controller/AccountController.ts +++ b/src/domain/account/controller/AccountController.ts @@ -34,13 +34,13 @@ export default function toRoutes( return parsed.data; }), async (ctx) => { - const { email, password } = ctx.req.valid("json"); - const account = await accountService.login(email, password); + const { username, password } = ctx.req.valid("json"); + const account = await accountService.login(username, password); return ctx.json({ success: true, accountId: account.id, - email: account.email, + username: account.username, }); }, ); @@ -56,13 +56,13 @@ export default function toRoutes( return parsed.data; }), async (ctx) => { - const { email, password } = ctx.req.valid("json"); - const account = await accountService.register(email, password); + const { username, password } = ctx.req.valid("json"); + const account = await accountService.register(username, password); return ctx.json({ success: true, accountId: account.id, - email: account.email, + username: account.username, }); }, ); diff --git a/src/domain/account/entity/AccountEntity.ts b/src/domain/account/entity/AccountEntity.ts index 843d6bf..1c2aac1 100644 --- a/src/domain/account/entity/AccountEntity.ts +++ b/src/domain/account/entity/AccountEntity.ts @@ -1,30 +1,22 @@ -import bcrypt from "bcrypt"; import { - InvalidEmailFormatError, + InvalidUsernameFormatError, WeakPasswordError, } from "../errors/AccountErrors"; import { - EMAIL_REGEX, + MAX_USERNAME_LENGTH, MIN_PASSWORD_LENGTH, + MIN_USERNAME_LENGTH, } from "../validation/AccountValidation"; export class AccountEntity { constructor( - public readonly id: string, - public readonly email: string, + public readonly id: number, + public readonly username: string, private password: string, public readonly roleId: number, public readonly createdAt: 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 { if (password.length < MIN_PASSWORD_LENGTH) { @@ -32,26 +24,41 @@ export class AccountEntity { } } - public verifyPassword(plainPassword: string): boolean { - return bcrypt.compareSync(plainPassword, this.password); + private static validateUsername(username: string): void { + if ( + username.length < MIN_USERNAME_LENGTH || + username.length > MAX_USERNAME_LENGTH + ) { + throw new InvalidUsernameFormatError(username); + } + } + + public async verifyPassword(plainPassword: string): Promise { + return await Bun.password.verify(plainPassword, this.password); } public get hashedPassword(): string { return this.password; } - static create(email: string, password: string): AccountEntity { + static async create( + username: string, + password: string, + ): Promise { AccountEntity.validatePassword(password); + AccountEntity.validateUsername(username); - const now = new Date(); - const id = crypto.randomUUID(); - const hashedPassword = bcrypt.hashSync(password, 10); + const hashedPassword = await Bun.password.hash(password); - return new AccountEntity(id, email, hashedPassword, 1, now, now); + return { + username, + hashedPassword, + roleId: 1, + }; } } -export type CreateAccountDto = { - email: string; - password: string; -}; +export type CreateAccountEntity = Omit< + AccountEntity, + "id" | "verifyPassword" | "createdAt" | "updatedAt" +>; diff --git a/src/domain/account/errors/AccountErrors.ts b/src/domain/account/errors/AccountErrors.ts index 9840903..9a84dbe 100644 --- a/src/domain/account/errors/AccountErrors.ts +++ b/src/domain/account/errors/AccountErrors.ts @@ -8,8 +8,8 @@ export class AccountNotFoundError extends HTTPError { } export class AccountAlreadyExistsError extends HTTPError { - constructor(email: string) { - super(409, `Un compte avec cet email existe déjà : ${email}`); + constructor(username: string) { + 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 { - constructor(email: string) { - super(400, `Format d'email invalide : ${email}`); +export class InvalidUsernameFormatError extends HTTPError { + constructor(username: string) { + super(400, `Format d'username invalide : ${username}`); } } diff --git a/src/domain/account/repository/AccountRepositoryInterface.ts b/src/domain/account/repository/AccountRepositoryInterface.ts index 9e8a6b2..a72f0b1 100644 --- a/src/domain/account/repository/AccountRepositoryInterface.ts +++ b/src/domain/account/repository/AccountRepositoryInterface.ts @@ -1,7 +1,10 @@ -import { AccountEntity } from "../entity/AccountEntity"; +import { + AccountEntity, + type CreateAccountEntity, +} from "../entity/AccountEntity"; export interface AccountRepositoryInterface { - findByEmail(email: string): Promise; - save(account: AccountEntity): Promise; - findById(id: string): Promise; + findByUsername(username: string): Promise; + insert(account: CreateAccountEntity): Promise; + findById(id: number): Promise; } diff --git a/src/domain/account/repository/AccoutRepository.ts b/src/domain/account/repository/AccoutRepository.ts index 582057f..aac2011 100644 --- a/src/domain/account/repository/AccoutRepository.ts +++ b/src/domain/account/repository/AccoutRepository.ts @@ -1,33 +1,36 @@ import type { DatabaseInterface } from "../../../database/DatabaseInterface"; -import { AccountEntity } from "../entity/AccountEntity"; +import { + AccountEntity, + type CreateAccountEntity, +} from "../entity/AccountEntity"; import type { AccountRepositoryInterface } from "./AccountRepositoryInterface"; export default class AccountRepository implements AccountRepositoryInterface { constructor(private readonly database: DatabaseInterface) {} - async findByEmail(email: string): Promise { - const sql = ` - SELECT id, email, password, role_id, created_at, updated_at - FROM accounts - WHERE email = $1 + async findByUsername(username: string): Promise { + const [result] = await this.database.sql< + { + id: number; + username: string; + password: string; + role_id: number; + created_at: Date; + updated_at: Date; + }[] + >` + SELECT id, username, password, role_id, created_at, updated_at + FROM accounts + WHERE username = ${username} `; - const result = await this.database.fetchOne<{ - id: string; - email: string; - password: string; - role_id: number; - created_at: Date; - updated_at: Date; - }>(sql, [email]); - if (!result) { return null; } return new AccountEntity( result.id, - result.email, + result.username, result.password, result.role_id, result.created_at, @@ -35,53 +38,39 @@ export default class AccountRepository implements AccountRepositoryInterface { ); } - async save(account: AccountEntity): Promise { - const sql = ` - INSERT INTO accounts (id, email, password, role_id, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (id) DO UPDATE SET - email = EXCLUDED.email, - password = EXCLUDED.password, - role_id = EXCLUDED.role_id, - updated_at = EXCLUDED.updated_at + async insert(account: CreateAccountEntity): Promise { + const [result] = await this.database.sql<{ id: number }[]>` + INSERT INTO accounts (username, password, role_id, created_at, updated_at) + VALUES (${account.username}, ${account.hashedPassword}, ${account.roleId}, NOW(), NOW()) 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; } - async findById(id: string): Promise { - const sql = ` - SELECT id, email, password, role_id, created_at, updated_at - FROM accounts - WHERE id = $1 + async findById(id: number): Promise { + const [result] = await this.database.sql< + { + id: number; + username: string; + password: string; + role_id: number; + created_at: Date; + updated_at: Date; + }[] + >` + SELECT id, username, password, role_id, created_at, updated_at + FROM accounts + WHERE id = ${id} `; - const result = await this.database.fetchOne<{ - id: string; - email: string; - password: string; - role_id: number; - created_at: Date; - updated_at: Date; - }>(sql, [id]); - if (!result) { return null; } return new AccountEntity( result.id, - result.email, + result.username, result.password, result.role_id, result.created_at, diff --git a/src/domain/account/service/AccountService.ts b/src/domain/account/service/AccountService.ts index 7a582b3..06844a0 100644 --- a/src/domain/account/service/AccountService.ts +++ b/src/domain/account/service/AccountService.ts @@ -10,30 +10,31 @@ import { export default class AccountService implements AccountServiceInterface { constructor(private readonly accountRepository: AccountRepositoryInterface) {} - async login(email: string, password: string): Promise { - const account = await this.accountRepository.findByEmail(email); + async login(username: string, password: string): Promise { + const account = await this.accountRepository.findByUsername(username); if (!account) { - throw new AccountNotFoundError(email); + throw new AccountNotFoundError(username); } - if (!account.verifyPassword(password)) { + if (!(await account.verifyPassword(password))) { throw new BadPasswordError(); } return account; } - async register(email: string, password: string): Promise { - const existingAccount = await this.accountRepository.findByEmail(email); + async register(username: string, password: string): Promise { + const existingAccount = + await this.accountRepository.findByUsername(username); if (existingAccount) { - throw new AccountAlreadyExistsError(email); + throw new AccountAlreadyExistsError(username); } - const newAccount = AccountEntity.create(email, password); - await this.accountRepository.save(newAccount); + const newAccount = await AccountEntity.create(username, password); + const newId = await this.accountRepository.insert(newAccount); - return newAccount; + return (await this.accountRepository.findById(newId)) as AccountEntity; } } diff --git a/src/domain/account/validation/AccountValidation.ts b/src/domain/account/validation/AccountValidation.ts index 22ee23c..df577f1 100644 --- a/src/domain/account/validation/AccountValidation.ts +++ b/src/domain/account/validation/AccountValidation.ts @@ -1,11 +1,19 @@ import { z } from "zod"; -export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 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() - .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 .string() @@ -15,11 +23,11 @@ export const passwordSchema = z ); export const loginSchema = z.object({ - email: emailSchema, + username: usernameSchema, password: passwordSchema, }); export const registerSchema = z.object({ - email: emailSchema, + username: usernameSchema, password: passwordSchema, }); diff --git a/src/middleware/rateLimiter.ts b/src/middleware/rateLimiter.ts index 294316d..352cceb 100644 --- a/src/middleware/rateLimiter.ts +++ b/src/middleware/rateLimiter.ts @@ -1,5 +1,5 @@ -import { Context, Next } from 'hono'; -import { TooManyAttemptsError } from '../domain/account/errors/AccountErrors'; +import type { Context, Next } from "hono"; +import { TooManyAttemptsError } from "../domain/account/errors/AccountErrors"; interface RateLimitConfig { windowMs: number; @@ -24,34 +24,38 @@ function cleanupExpiredEntries(): void { } 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) => { const key = keyGenerator(c); const now = Date.now(); - + cleanupExpiredEntries(); - + const record = attempts.get(key); - + if (!record) { attempts.set(key, { count: 1, resetTime: now + windowMs }); await next(); return; } - + if (now > record.resetTime) { attempts.set(key, { count: 1, resetTime: now + windowMs }); await next(); return; } - + if (record.count >= maxAttempts) { const retryAfter = Math.ceil((record.resetTime - now) / 1000); throw new TooManyAttemptsError(retryAfter); } - + record.count++; await next(); }; -} \ No newline at end of file +} diff --git a/tests/AccountEntity.test.ts b/tests/AccountEntity.test.ts deleted file mode 100644 index 12c56cd..0000000 --- a/tests/AccountEntity.test.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/tests/AccountRepository.test.ts b/tests/AccountRepository.test.ts deleted file mode 100644 index 14e9c4e..0000000 --- a/tests/AccountRepository.test.ts +++ /dev/null @@ -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; - fetchAll: ReturnType; - fetchOne: ReturnType; - execute: ReturnType; - }; - - 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, - ]), - ); - }); - }); -}); diff --git a/tests/AccountService.test.ts b/tests/AccountService.test.ts deleted file mode 100644 index 92e1040..0000000 --- a/tests/AccountService.test.ts +++ /dev/null @@ -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; - save: ReturnType; - findById: ReturnType; - }; - - 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, - ); - }); - }); -}); diff --git a/tests/account/AccountEntity.test.ts b/tests/account/AccountEntity.test.ts new file mode 100644 index 0000000..96cbe86 --- /dev/null +++ b/tests/account/AccountEntity.test.ts @@ -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); + }); + }); +}); diff --git a/tests/account/AccountRepository.test.ts b/tests/account/AccountRepository.test.ts new file mode 100644 index 0000000..c094072 --- /dev/null +++ b/tests/account/AccountRepository.test.ts @@ -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); + }); + }); +}); diff --git a/tests/account/AccountService.test.ts b/tests/account/AccountService.test.ts new file mode 100644 index 0000000..ff66037 --- /dev/null +++ b/tests/account/AccountService.test.ts @@ -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; + insert: ReturnType; + findById: ReturnType; + }; + + 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, + ); + }); + }); +}); diff --git a/tests/AccountValidation.test.ts b/tests/account/AccountValidation.test.ts similarity index 55% rename from tests/AccountValidation.test.ts rename to tests/account/AccountValidation.test.ts index c4b15ae..2a932bc 100644 --- a/tests/AccountValidation.test.ts +++ b/tests/account/AccountValidation.test.ts @@ -1,62 +1,14 @@ import { describe, it, expect } from "bun:test"; import { - emailSchema, passwordSchema, loginSchema, registerSchema, - EMAIL_REGEX, MIN_PASSWORD_LENGTH, -} from "../src/domain/account/validation/AccountValidation"; -import { AccountEntity } from "../src/domain/account/entity/AccountEntity"; -import { - InvalidEmailFormatError, - WeakPasswordError, -} from "../src/domain/account/errors/AccountErrors"; +} from "../../src/domain/account/validation/AccountValidation"; +import { WeakPasswordError } from "../../src/domain/account/errors/AccountErrors"; +import { AccountEntity } from "../../src/domain/account/entity/AccountEntity"; 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", () => { const validPasswords = ["password123", "abcdefgh", "12345678", "P@ssw0rd!"]; @@ -91,8 +43,11 @@ describe("AccountValidation", () => { describe("Complete schemas", () => { it("should validate login schema correctly", () => { - const validLogin = { email: "test@example.com", password: "password123" }; - const invalidLogin = { email: "invalid", password: "123" }; + const validLogin = { + username: "testaccount", + password: "password123", + }; + const invalidLogin = { username: "ii", password: "123" }; expect(loginSchema.safeParse(validLogin).success).toBe(true); expect(loginSchema.safeParse(invalidLogin).success).toBe(false); @@ -100,10 +55,10 @@ describe("AccountValidation", () => { it("should validate register schema correctly", () => { const validRegister = { - email: "test@example.com", + username: "testaccount", password: "password123", }; - const invalidRegister = { email: "invalid", password: "123" }; + const invalidRegister = { username: "ii", password: "123" }; expect(registerSchema.safeParse(validRegister).success).toBe(true); expect(registerSchema.safeParse(invalidRegister).success).toBe(false); @@ -121,11 +76,17 @@ describe("AccountValidation", () => { } }); - it("should return consistent email error message", () => { - const result = emailSchema.safeParse("invalid-email"); + it("should return consistent username error message", () => { + const result = registerSchema.safeParse({ + username: "ii", + password: "validPassword123", + }); + expect(result.success).toBe(false); 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", + ); } }); }); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 0000000..77482fc --- /dev/null +++ b/tests/utils.ts @@ -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 { + 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; +}