From 15b4784e47dd3b9844d3693f3eaf06c085626e2b Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 17:48:36 -0300 Subject: [PATCH 01/14] feat: instala `vitest`, `fake-js`, `async-retry` e implementa scripts de test --- package-lock.json | 1518 +++++++++++++++++++++++++++++++++++++++++---- package.json | 23 +- 2 files changed, 1426 insertions(+), 115 deletions(-) diff --git a/package-lock.json b/package-lock.json index deeb657..374bc10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,14 +21,18 @@ "zod": "4.1.8" }, "devDependencies": { + "@faker-js/faker": "10.4.0", + "@types/async-retry": "^1.4.9", "@types/bcrypt": "6.0.0", "@types/cookie-parser": "1.4.9", "@types/cors": "2.8.19", "@types/express": "5.0.3", + "async-retry": "1.3.3", "concurrently": "9.2.1", "dotenv-cli": "11.0.0", "prisma": "7.7.0", - "tsx": "4.21.0" + "tsx": "4.21.0", + "vitest": "4.1.4" }, "engines": { "node": "24" @@ -64,6 +68,40 @@ "@electric-sql/pglite": "0.4.1" } }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -506,6 +544,23 @@ "node": ">=18" } }, + "node_modules/@faker-js/faker": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.4.0.tgz", + "integrity": "sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" + } + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", @@ -519,6 +574,13 @@ "hono": "^4" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", @@ -526,6 +588,35 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@prisma/adapter-pg": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.7.0.tgz", @@ -883,6 +974,270 @@ } } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "dev": true, + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -890,6 +1245,27 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/async-retry": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/async-retry/-/async-retry-1.4.9.tgz", + "integrity": "sha512-s1ciZQJzRh3708X/m3vPExr5KJlzlZJvXsKpbtE2luqNcbROr64qU+3KpJsYHqWMeaxI839OvXf9PrUSw1Xtyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "*" + } + }, "node_modules/@types/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", @@ -911,6 +1287,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -941,6 +1328,20 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", @@ -1018,7 +1419,14 @@ "csstype": "^3.2.2" } }, - "node_modules/@types/send": { + "node_modules/@types/retry": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", @@ -1039,6 +1447,119 @@ "@types/node": "*" } }, + "node_modules/@vitest/expect": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.4", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1069,16 +1590,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1095,6 +1606,36 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/async-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/aws-ssl-profiles": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", @@ -1230,6 +1771,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1247,19 +1798,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/chart.js": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", @@ -1314,6 +1852,69 @@ "node": ">=12" } }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1359,6 +1960,22 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", @@ -1398,6 +2015,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -1522,6 +2146,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "17.2.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", @@ -1608,13 +2242,6 @@ "fast-check": "^3.23.1" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/empathic": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", @@ -1665,6 +2292,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -1735,6 +2369,16 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1744,6 +2388,16 @@ "node": ">= 0.6" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -1825,6 +2479,23 @@ "node": ">=8.0.0" } }, + "node_modules/fast-check/node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1849,6 +2520,24 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -1994,9 +2683,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -2092,9 +2781,9 @@ } }, "node_modules/hono": { - "version": "4.12.12", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", - "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "version": "4.12.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", + "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "devOptional": true, "license": "MIT", "engines": { @@ -2206,21 +2895,282 @@ "devOptional": true, "license": "MIT" }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/lru.min": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", - "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "devOptional": true, - "license": "MIT", + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { - "bun": ">=1.0.0", + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", "deno": ">=1.30.0", "node": ">=8.0.0" }, @@ -2229,6 +3179,16 @@ "url": "https://github.com/sponsors/wellwelwel" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2334,6 +3294,25 @@ "node": ">=8.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2416,6 +3395,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -2585,6 +3575,26 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkg-types": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", @@ -2597,6 +3607,35 @@ "pathe": "^2.0.3" } }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", @@ -2716,23 +3755,6 @@ "node": ">= 0.10" } }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, "node_modules/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", @@ -2872,6 +3894,40 @@ "node": ">= 4" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -3077,6 +4133,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -3090,6 +4153,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -3109,6 +4182,13 @@ "node": ">= 0.6" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3125,58 +4205,61 @@ "devOptional": true, "license": "MIT" }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "has-flag": "^4.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "devOptional": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=10" + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyexec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", - "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", - "devOptional": true, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, "node_modules/toidentifier": { @@ -3278,6 +4361,181 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.15", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3294,22 +4552,21 @@ "node": ">= 8" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "siginfo": "^2.0.0", + "stackback": "0.0.2" }, - "engines": { - "node": ">=10" + "bin": { + "why-is-node-running": "cli.js" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "engines": { + "node": ">=8" } }, "node_modules/wrappy": { @@ -3366,6 +4623,51 @@ "node": ">=12" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zeptomatch": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", diff --git a/package.json b/package.json index a78b5c5..1ed9f85 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,21 @@ "version": "1.0.0", "description": "- **Node.js + Express**\r - **TypeScript**\r - **PostgreSQL** (com Prisma)\r - **JWT** (autenticação)\r - **Zod** (validação de dados)", "main": "index.js", + "type": "module", "scripts": { - "services:up": "docker compose -f src/infra/compose.yaml up -d", + "dev": "npm run services:up && npm run migrations:up && npm run migrations:generate && npm run server:watch", + "services:up": "docker compose -f src/infra/compose.yaml up -d && npm run services:wait:database ", + "services:down": "docker compose -f src/infra/compose.yaml down -v", "services:wait:database": "node src/infra/scripts/wait-for-postgres.ts", - "migrations:up": "dotenv -e .env -- npx prisma migrate dev", + "migrations:create": "dotenv -e .env -- prisma migrate dev --name", + "migrations:up": "dotenv -e .env -- prisma migrate dev", + "migrations:reset": "dotenv -e .env -- prisma migrate reset --force", + "migrations:generate": "prisma generate", "server:watch": "tsx --watch src/server.ts", - "dev": "npm run services:up && npm run services:wait:database && npm run migrations:up && npm run server:watch", - "services:down": "docker compose -f src/infra/compose.yaml down -v", - "migrations:create": "npx prisma migrate dev --name", "start": "node ./dist/server.js", - "postinstall": "prisma generate" + "test": "npm run services:up && npm run migrations:up && npm run migrations:generate && concurrently -k -s first -n \"server,test\" --hide server \"tsx src/server.ts\" \"vitest", + "test:watch": "vitest", + "postinstall": "npm run migrations:generate" }, "repository": { "type": "git", @@ -37,14 +42,18 @@ "zod": "4.1.8" }, "devDependencies": { + "@faker-js/faker": "10.4.0", + "@types/async-retry": "^1.4.9", "@types/bcrypt": "6.0.0", "@types/cookie-parser": "1.4.9", "@types/cors": "2.8.19", "@types/express": "5.0.3", + "async-retry": "1.3.3", "concurrently": "9.2.1", "dotenv-cli": "11.0.0", "prisma": "7.7.0", - "tsx": "4.21.0" + "tsx": "4.21.0", + "vitest": "4.1.4" }, "engines": { "node": "24" From 354bd3699e9d511a3b80620557b814fb2c1b6162 Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 17:49:06 -0300 Subject: [PATCH 02/14] feat: implementa setup de test com o `vitest` --- tsconfig.json | 7 ++++--- vitest.config.ts | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 vitest.config.ts diff --git a/tsconfig.json b/tsconfig.json index 6954797..b47a199 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,10 +14,11 @@ "rootDir": ".", "paths": { "~/*": ["./src/*"], - "@/*": ["./prisma/*"] + "@/*": ["./prisma/*"], + "$/*": ["./tests/*"] }, - "types": ["node"] + "types": ["node", "vitest/globals"] }, - "include": ["src/**/*"], + "include": ["src", "tests", "prisma"], "exclude": ["node_modules", "dist"] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..3699152 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; +import path from "path"; + +export default defineConfig({ + test: { + alias: { + "~": path.resolve(__dirname, "./src"), + "@": path.resolve(__dirname, "./prisma"), + $: path.resolve(__dirname, "./tests"), + }, + testTimeout: 60_000, + environment: "node", + globals: true, + pool: "threads", + maxConcurrency: 1, + exclude: ["node_modules", "dist", ".idea", ".git", ".cache"], + }, +}); From ddfcdeda79b72f3d6459a16100177896aeb7b56f Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 17:49:57 -0300 Subject: [PATCH 03/14] refactor: modifica schema do prisma e altera nome das migrations --- .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 12 ++++++++++++ .../migration.sql | 16 ++++++++++++++++ .../migration.sql | 2 ++ prisma/schema.prisma | 15 ++++++++------- 6 files changed, 38 insertions(+), 7 deletions(-) rename prisma/migrations/{20260412034601_altera => 20260412034601_altera_task_user_usersession_set_id_uuid}/migration.sql (100%) rename prisma/migrations/{20260412035239_altera => 20260412035239_altera_task_add_updated_at}/migration.sql (100%) create mode 100644 prisma/migrations/20260416201921_altera_user_limita_colunas_name_email_password/migration.sql create mode 100644 prisma/migrations/20260416234204_altera_user_padroniza_colunas/migration.sql create mode 100644 prisma/migrations/20260416235258_altera_usersession_set_default_created_at/migration.sql diff --git a/prisma/migrations/20260412034601_altera/migration.sql b/prisma/migrations/20260412034601_altera_task_user_usersession_set_id_uuid/migration.sql similarity index 100% rename from prisma/migrations/20260412034601_altera/migration.sql rename to prisma/migrations/20260412034601_altera_task_user_usersession_set_id_uuid/migration.sql diff --git a/prisma/migrations/20260412035239_altera/migration.sql b/prisma/migrations/20260412035239_altera_task_add_updated_at/migration.sql similarity index 100% rename from prisma/migrations/20260412035239_altera/migration.sql rename to prisma/migrations/20260412035239_altera_task_add_updated_at/migration.sql diff --git a/prisma/migrations/20260416201921_altera_user_limita_colunas_name_email_password/migration.sql b/prisma/migrations/20260416201921_altera_user_limita_colunas_name_email_password/migration.sql new file mode 100644 index 0000000..01f93e0 --- /dev/null +++ b/prisma/migrations/20260416201921_altera_user_limita_colunas_name_email_password/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to alter the column `email` on the `User` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(254)`. + - You are about to alter the column `password` on the `User` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(60)`. + - You are about to alter the column `name` on the `User` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(30)`. + +*/ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "email" SET DATA TYPE VARCHAR(254), +ALTER COLUMN "password" SET DATA TYPE VARCHAR(60), +ALTER COLUMN "name" SET DATA TYPE VARCHAR(30); diff --git a/prisma/migrations/20260416234204_altera_user_padroniza_colunas/migration.sql b/prisma/migrations/20260416234204_altera_user_padroniza_colunas/migration.sql new file mode 100644 index 0000000..fcc339b --- /dev/null +++ b/prisma/migrations/20260416234204_altera_user_padroniza_colunas/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `createdAt` on the `User` table. All the data in the column will be lost. + - You are about to drop the column `updatedAt` on the `User` table. All the data in the column will be lost. + - Added the required column `created_at` to the `UserSession` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" DROP COLUMN "createdAt", +DROP COLUMN "updatedAt", +ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "UserSession" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL; diff --git a/prisma/migrations/20260416235258_altera_usersession_set_default_created_at/migration.sql b/prisma/migrations/20260416235258_altera_usersession_set_default_created_at/migration.sql new file mode 100644 index 0000000..8a1c0c9 --- /dev/null +++ b/prisma/migrations/20260416235258_altera_usersession_set_default_created_at/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "UserSession" ALTER COLUMN "created_at" SET DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2ef5215..40c71c7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,12 +8,12 @@ datasource db { } model User { - id String @id @default(uuid()) - name String - email String @unique - password String - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) + id String @id @default(uuid()) + name String @db.VarChar(30) + email String @unique @db.VarChar(254) + password String @db.VarChar(60) + created_at DateTime @default(now()) + updated_at DateTime @default(now()) sessions UserSession[] tasks Task[] @@ -22,6 +22,7 @@ model User { model UserSession { id String @id user_id String + created_at DateTime @default(now()) expires_at DateTime last_active_at DateTime @@ -41,7 +42,7 @@ model Task { status TaskStatus @default(pending) user_id String created_at DateTime @default(now()) - updated_at DateTime @default(now()) + updated_at DateTime @default(now()) user User @relation(fields: [user_id], references: [id]) } From 469f0b8df014854225afdc297459d82cd19639ba Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 17:51:54 -0300 Subject: [PATCH 04/14] =?UTF-8?q?refactor:=20refatora=C3=A7=C3=B5es=20gera?= =?UTF-8?q?is=20nas=20importa=C3=A7=C3=B5es=20e=20ajutes=20relacionados=20?= =?UTF-8?q?as=20mudan=C3=A7as=20do=20banco=20de=20dados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/task.ts | 26 ++++++++++++++++++-------- src/controller/user.ts | 23 +++++++++++------------ src/index.ts | 2 +- src/infra/database.ts | 4 ++-- src/middleware/authenticate.ts | 4 ++-- src/models/cookieResponse.ts | 4 ++-- src/models/customResponse.ts | 8 ++++---- src/models/password.ts | 2 +- src/models/session.ts | 18 ++++++++++-------- src/models/task.ts | 6 +++--- src/models/user.ts | 24 ++++++++---------------- src/router/router.ts | 8 ++++---- src/server.ts | 2 +- src/zod/taskSchema.ts | 2 +- src/zod/userSchema.ts | 4 ++-- 15 files changed, 70 insertions(+), 67 deletions(-) diff --git a/src/controller/task.ts b/src/controller/task.ts index 89bbc83..b21a49c 100644 --- a/src/controller/task.ts +++ b/src/controller/task.ts @@ -1,10 +1,14 @@ import { Response } from "express"; -import { Task } from "@/generated/client"; -import { idTaskSchema, taskSchema, updateTaskSchema } from "~/zod/taskSchema"; -import { AuthenticatedRequest } from "~/middleware/authenticate"; -import { canRequest } from "~/models/authorization"; -import { CustomResponse } from "~/models/customResponse"; -import { taskModel } from "~/models/task"; +import { Task } from "@/generated/client.js"; +import { + idTaskSchema, + taskSchema, + updateTaskSchema, +} from "~/zod/taskSchema.js"; +import { AuthenticatedRequest } from "~/middleware/authenticate.js"; +import { canRequest } from "~/models/authorization.js"; +import { CustomResponse } from "~/models/customResponse.js"; +import { taskModel } from "~/models/task.js"; export const getAllTasks = async (req: AuthenticatedRequest, res: Response) => { const response = new CustomResponse(res); @@ -40,7 +44,10 @@ export const insertTask = async (req: AuthenticatedRequest, res: Response) => { try { const task = await taskModel.create(result.data, user.id); - response.created(`Task ${task.id} adicionada com sucesso!`, task); + response.created({ + message: `Task ${task.id} adicionada com sucesso!`, + data: task, + }); } catch (error) { console.error("Erro ao inserir task: ", error); response.internalServerError(); @@ -65,7 +72,10 @@ export const updateTask = async (req: AuthenticatedRequest, res: Response) => { if (!userCanRequest(req, response, task)) return; const updatedTask = await taskModel.update(taskId, result.data); - response.created(`Task ${taskId} atualizada.`, updatedTask); + response.created({ + message: `Task ${taskId} atualizada.`, + data: updatedTask, + }); } catch (error) { console.error("Erro ao atualizar tasks: ", error); response.internalServerError(); diff --git a/src/controller/user.ts b/src/controller/user.ts index 3265551..1476d9c 100644 --- a/src/controller/user.ts +++ b/src/controller/user.ts @@ -1,13 +1,12 @@ import { Request, Response } from "express"; -import { loginSchema } from "~/zod/userSchema"; -import { registerSchema } from "~/zod/userSchema"; -import { AuthenticatedUser, validateSession } from "~/models/session"; -import { registerUser, login } from "~/models/user"; -import { CustomResponse } from "~/models/customResponse"; -import { CookieResponse } from "~/models/cookieResponse"; +import { loginSchema } from "~/zod/userSchema.js"; +import { registerSchema } from "~/zod/userSchema.js"; +import { AuthenticatedUser, validateSession } from "~/models/session.js"; +import { registerUser, login } from "~/models/user.js"; +import { CookieResponse } from "~/models/cookieResponse.js"; export const register = async (req: Request, res: Response) => { - const response = new CustomResponse(res); + const response = new CookieResponse(res); const result = registerSchema.safeParse(req.body); if (result.error) { @@ -17,7 +16,7 @@ export const register = async (req: Request, res: Response) => { try { const registeredUser = await registerUser(result.data); - response.success({ + response.created({ message: `Usuário '${registeredUser.name}' criado com sucesso!`, }); } catch (error: any) { @@ -29,14 +28,14 @@ export const register = async (req: Request, res: Response) => { export const authentication = async (req: Request, res: Response) => { const result = loginSchema.safeParse(req.body); - const response = new CustomResponse(res); + const response = new CookieResponse(res); if (result.error) { response.badRequest(result.error); return; } try { const sessionObject = await login(result.data); - if (!sessionObject.success) { + if (!sessionObject) { response.unauthorized( "Verifique os dados enviados e tente novamente", "Email ou senha inválido", @@ -44,8 +43,8 @@ export const authentication = async (req: Request, res: Response) => { return; } - new CookieResponse(res).setCookie("session", sessionObject.session); - response.success({ message: "Usuário autenticado!" }); + new CookieResponse(res).setCookie("session", sessionObject.id); + response.created({ message: "Usuário autenticado!" }); } catch (error) { console.error("Erro insperado ao efetuar login: ", error); response.internalServerError(); diff --git a/src/index.ts b/src/index.ts index 3f533ac..6ae84da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import express from "express"; import cors from "cors"; import cookieParser from "cookie-parser"; -import router from "~/router/router"; +import router from "~/router/router.js"; export const app = express(); app.use(cors({ credentials: true })); diff --git a/src/infra/database.ts b/src/infra/database.ts index 65f9fac..a206f99 100644 --- a/src/infra/database.ts +++ b/src/infra/database.ts @@ -1,6 +1,6 @@ import { PrismaPg } from "@prisma/adapter-pg"; -import { PrismaClient } from "@/generated/client"; -import { env } from "~/config/env"; +import { PrismaClient } from "@/generated/client.js"; +import { env } from "~/config/env.js"; export const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: env.DATABASE_URL }), diff --git a/src/middleware/authenticate.ts b/src/middleware/authenticate.ts index e36a07d..b8c863e 100644 --- a/src/middleware/authenticate.ts +++ b/src/middleware/authenticate.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; -import { CookieResponse } from "~/models/cookieResponse"; -import { AuthenticatedUser, validateSession } from "~/models/session"; +import { CookieResponse } from "~/models/cookieResponse.js"; +import { AuthenticatedUser, validateSession } from "~/models/session.js"; export interface AuthenticatedRequest extends Request { context?: { diff --git a/src/models/cookieResponse.ts b/src/models/cookieResponse.ts index 4209dbb..dde3c49 100644 --- a/src/models/cookieResponse.ts +++ b/src/models/cookieResponse.ts @@ -1,6 +1,6 @@ import { CookieOptions } from "express"; -import { CustomResponse } from "./customResponse"; -import { env } from "~/config/env"; +import { CustomResponse } from "./customResponse.js"; +import { env } from "~/config/env.js"; export class CookieResponse extends CustomResponse { private cookieOptions: CookieOptions = { diff --git a/src/models/customResponse.ts b/src/models/customResponse.ts index 86e9a8d..7d06c61 100644 --- a/src/models/customResponse.ts +++ b/src/models/customResponse.ts @@ -44,18 +44,18 @@ export class CustomResponse { badRequest(error: any) { const errorMessage = this.errorHandler(error); - this.response.status(400).json({ message: errorMessage }); + this.response.status(400).json({ error: errorMessage }); } success(data: object) { this.response.status(200).json(data); } - created(message: string, data?: any) { - this.response.status(201).json({ message, data }); + created(data?: object) { + this.response.status(201).json(data); } noContent() { - this.response.status(204); + this.response.status(204).json(); } } diff --git a/src/models/password.ts b/src/models/password.ts index 5a7a276..a34ff02 100644 --- a/src/models/password.ts +++ b/src/models/password.ts @@ -1,5 +1,5 @@ import { hash, compare } from "bcrypt"; -import { env } from "~/config/env"; +import { env } from "~/config/env.js"; export const hashPassword = async (password: string) => { const salt = genSalt(); diff --git a/src/models/session.ts b/src/models/session.ts index e96ab12..9bd3d69 100644 --- a/src/models/session.ts +++ b/src/models/session.ts @@ -1,12 +1,12 @@ import { randomUUID } from "node:crypto"; -import { env } from "~/config/env"; -import { prisma } from "~/infra/database"; +import { env } from "~/config/env.js"; +import { prisma } from "~/infra/database.js"; export const createSession = async (userId: string) => { const sessionId = randomUUID(); const now = new Date(); - await prisma.userSession.create({ + const session = await prisma.userSession.create({ data: { id: sessionId, user_id: userId, @@ -14,7 +14,7 @@ export const createSession = async (userId: string) => { last_active_at: now, }, }); - return sessionId; + return session; }; export const validateSession = async (sessionId: string) => { @@ -43,8 +43,8 @@ export const validateSession = async (sessionId: string) => { data: { last_active_at: now }, }); - const { email, id, name, createdAt, updatedAt } = userDb; - const secureObjectValue = { id, email, name, createdAt, updatedAt }; + const { email, id, name, created_at, updated_at } = userDb; + const secureObjectValue = { id, email, name, created_at, updated_at }; return secureObjectValue; }; @@ -52,6 +52,8 @@ export type AuthenticatedUser = { id: string; email: string; name: string; - createdAt: Date; - updatedAt: Date; + created_at: Date; + updated_at: Date; }; + +export type UserSession = Awaited>; diff --git a/src/models/task.ts b/src/models/task.ts index 13f9da2..8008149 100644 --- a/src/models/task.ts +++ b/src/models/task.ts @@ -1,6 +1,6 @@ -import { TaskDelegate } from "@/generated/models"; -import { prisma } from "~/infra/database"; -import { Task as TaskEnt, UpdateTask } from "~/zod/taskSchema"; +import { TaskDelegate } from "@/generated/models.js"; +import { prisma } from "~/infra/database.js"; +import { Task as TaskEnt, UpdateTask } from "~/zod/taskSchema.js"; class TaskModel { private model: TaskDelegate; diff --git a/src/models/user.ts b/src/models/user.ts index 52b14a2..03eec83 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -1,7 +1,7 @@ -import { LoginInput, UserInput } from "~/zod/userSchema"; -import { createSession } from "~/models/session"; -import { comparePassword, hashPassword } from "./password"; -import { prisma } from "~/infra/database"; +import { LoginInput, UserInput } from "~/zod/userSchema.js"; +import { createSession } from "~/models/session.js"; +import { comparePassword, hashPassword } from "./password.js"; +import { prisma } from "~/infra/database.js"; export const registerUser = async (userInputValues: UserInput) => { const password = await hashPassword(userInputValues.password); @@ -13,29 +13,21 @@ export const registerUser = async (userInputValues: UserInput) => { }; export const login = async (userInputValues: LoginInput) => { - const result = { - session: null, - success: false, - }; - const user = await prisma.user.findUnique({ where: { email: userInputValues.email }, }); - if (!user) { - return result; - } + if (!user) return false; const isValid = await comparePassword( userInputValues.password, user.password, ); - if (!isValid) { - return result; - } + if (!isValid) return false; const session = await createSession(user.id); + if (!session) return false; - return { session, success: true }; + return session; }; diff --git a/src/router/router.ts b/src/router/router.ts index 666096b..58e5234 100644 --- a/src/router/router.ts +++ b/src/router/router.ts @@ -1,9 +1,9 @@ import { Router } from "express"; -import { index } from "~/controller"; -import * as userCtrl from "~/controller/user"; -import * as taskCtrl from "~/controller/task"; +import { index } from "~/controller/index.js"; +import * as userCtrl from "~/controller/user.js"; +import * as taskCtrl from "~/controller/task.js"; -import { authenticate } from "~/middleware/authenticate"; +import { authenticate } from "~/middleware/authenticate.js"; const router = Router(); diff --git a/src/server.ts b/src/server.ts index c610667..d50e695 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,3 +1,3 @@ -import { app } from "."; +import { app } from "./index.js"; app.listen(5001, () => console.log("🚀 Servidor on na porta 5001")); diff --git a/src/zod/taskSchema.ts b/src/zod/taskSchema.ts index ff00716..2f74a87 100644 --- a/src/zod/taskSchema.ts +++ b/src/zod/taskSchema.ts @@ -4,7 +4,7 @@ z.config(pt()); export const taskSchema = z.object({ title: z.string("Coluna 'title' é obrigatório!"), - description: z.string("coluna 'description' é obrigatória!"), + description: z.string("Coluna 'description' é obrigatória!"), status: z.enum(["pending", "in_progress", "done"]).optional(), }); diff --git a/src/zod/userSchema.ts b/src/zod/userSchema.ts index 694a52a..40b4f24 100644 --- a/src/zod/userSchema.ts +++ b/src/zod/userSchema.ts @@ -6,12 +6,12 @@ export const registerSchema = z.object({ name: z .string("Nome é obrigatório.") .min(3, "Nome deve ter no mínimo 3 caracteres.") - .max(20, "Nome deve ter no máximo 20 caracteres."), + .max(30, "Nome deve ter no máximo 30 caracteres."), email: z.email("Email não enviado ou inválido."), password: z .string("Senha é obrigátoria.") .min(8, "Senha deve ter no mínimo 8 caracteres.") - .max(15, "Senha deve ter no máximo 15 caracteres."), + .max(60, "Senha deve ter no máximo 60 caracteres."), }); export type UserInput = z.infer; From 1f38fd0cea672298df565e0c56fe558567d76bfc Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 17:52:51 -0300 Subject: [PATCH 05/14] feat: implementa script de para executar ou resetar as `migrations` --- src/infra/scripts/migrator.ts | 9 +++++++ src/infra/scripts/runScript.ts | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/infra/scripts/migrator.ts create mode 100644 src/infra/scripts/runScript.ts diff --git a/src/infra/scripts/migrator.ts b/src/infra/scripts/migrator.ts new file mode 100644 index 0000000..eaf7249 --- /dev/null +++ b/src/infra/scripts/migrator.ts @@ -0,0 +1,9 @@ +import { runScript } from "./runScript.js"; + +export async function resetDatabase() { + await runScript("migrations:reset"); +} + +export async function runPendingMigrations() { + await runScript("migrations:up"); +} diff --git a/src/infra/scripts/runScript.ts b/src/infra/scripts/runScript.ts new file mode 100644 index 0000000..d54a785 --- /dev/null +++ b/src/infra/scripts/runScript.ts @@ -0,0 +1,44 @@ +import { spawn } from "node:child_process"; + +function npmBin() { + return process.platform === "win32" ? "npm.cmd" : "npm"; +} + +function cleanServices(service: string) { + console.log(`Serviço '${service}' encerrado`); + const stopServices = spawn(npmBin(), ["run", "services:stop"], { + stdio: "inherit", + shell: true, + }); + stopServices.on("exit", () => { + console.log("Serviços finalizados com sucesso!"); + }); +} + +export function runScript(script: string, service?: string): Promise { + return new Promise((resolve, reject) => { + const runStartScript = spawn(npmBin(), ["run", script], { + shell: true, + stdio: "inherit", + }); + + runStartScript.on("error", (err) => { + reject(err); + }); + + runStartScript.on("exit", (code) => { + if (service) cleanServices(service); + + if (code === 0) resolve(); + else reject(new Error(`Script ${script} falhou com codigo ${code}`)); + }); + + const handleSignal = (signal: NodeJS.Signals) => { + if (runStartScript.killed) return; + runStartScript.kill(signal); + }; + + process.on("SIGINT", () => handleSignal("SIGINT")); + process.on("SIGTERM", () => handleSignal("SIGTERM")); + }); +} From 970085a71ec54faf66586ab71792a5f2bbcc3956 Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 17:54:15 -0300 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20implementa=20testes=20automatizad?= =?UTF-8?q?os=20de=20integra=C3=A7=C3=A3o=20para=20as=20rotas=20existentes?= =?UTF-8?q?=20da=20aplica=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/controller/task/delete.test.ts | 122 ++++++++++++++++++ tests/controller/task/get.test.ts | 123 ++++++++++++++++++ tests/controller/task/patch.test.ts | 182 +++++++++++++++++++++++++++ tests/controller/task/post.test.ts | 163 ++++++++++++++++++++++++ tests/controller/user/get.test.ts | 122 ++++++++++++++++++ tests/controller/user/post.test.ts | 173 +++++++++++++++++++++++++ tests/orchestrator.ts | 51 ++++++++ 7 files changed, 936 insertions(+) create mode 100644 tests/controller/task/delete.test.ts create mode 100644 tests/controller/task/get.test.ts create mode 100644 tests/controller/task/patch.test.ts create mode 100644 tests/controller/task/post.test.ts create mode 100644 tests/controller/user/get.test.ts create mode 100644 tests/controller/user/post.test.ts create mode 100644 tests/orchestrator.ts diff --git a/tests/controller/task/delete.test.ts b/tests/controller/task/delete.test.ts new file mode 100644 index 0000000..3f73c0a --- /dev/null +++ b/tests/controller/task/delete.test.ts @@ -0,0 +1,122 @@ +import { UserSession } from "@/generated/client.js"; +import { AuthenticatedUser } from "~/models/session.js"; +import { + API, + createSession, + createTask, + createUser, + waitForAllServices, +} from "$/orchestrator.js"; + +beforeAll(async () => { + await waitForAllServices(); +}); + +describe("DELETE `/tasks`", () => { + test("Sem usuário logado", async () => { + const fakeTaskId = crypto.randomUUID(); + + const response = await fetch(`${API.task}/${fakeTaskId}`, { + method: "DELETE", + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(401); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + message: "Não autorizado.", + action: "Faça o login.", + }); + }); + + let defaultUser: AuthenticatedUser; + let defaultSession: UserSession; + test("Com usuário logado porém `task` com ID inválido", async () => { + const idInvalido = "id-no-formato-invalido"; + defaultUser = await createUser(); + defaultSession = await createSession(defaultUser); + const response = await fetch(`${API.task}/${idInvalido}`, { + method: "DELETE", + headers: { + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + error: "Id da task deve ser do tipo UUID.", + }); + }); + + test("Com usuário logado porém `task` com ID válido porém inexistente no banco", async () => { + const fakeTaskId = crypto.randomUUID(); + + const response = await fetch(`${API.task}/${fakeTaskId}`, { + method: "DELETE", + headers: { + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(true); + expect(response.status).toBe(204); + }); + + test("Com usuário logado e `task` com ID existente", async () => { + const task = await createTask( + { + title: "Teste deletar", + description: "Enviando uma task existente para deletar!", + }, + defaultUser, + ); + + const response = await fetch(`${API.task}/${task.id}`, { + method: "DELETE", + headers: { + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(true); + expect(response.status).toBe(200); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + message: `Task ${task.id} deletada com sucesso!`, + }); + }); + + test("Com um usuário tentando excluir `task` de outro usuário", async () => { + const task = await createTask( + { + title: "Task do usuário padrão", + description: + "Enviando uma task existente para outro usuário tentar deletar!", + }, + defaultUser, + ); + + const otherUser = await createUser(); + const otherSession = await createSession(otherUser); + + const response = await fetch(`${API.task}/${task.id}`, { + method: "DELETE", + headers: { + Cookie: `session=${otherSession.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(403); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + message: "Voce não tem permissão executar essa ação.", + }); + }); +}); diff --git a/tests/controller/task/get.test.ts b/tests/controller/task/get.test.ts new file mode 100644 index 0000000..19f0ba2 --- /dev/null +++ b/tests/controller/task/get.test.ts @@ -0,0 +1,123 @@ +import { Task, UserSession } from "@/generated/client.js"; +import { AuthenticatedUser } from "~/models/session.js"; +import { + API, + createSession, + createTask, + createUser, + waitForAllServices, +} from "$/orchestrator.js"; + +beforeAll(async () => { + await waitForAllServices(); +}); + +let newUser: AuthenticatedUser; +let session: UserSession; +let newTask: Task; +describe("GET `/tasks`", () => { + test("Sem usuário logado", async () => { + const response = await fetch(API.task); + + expect(response.ok).toBe(false); + expect(response.status).toBe(401); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + message: "Não autorizado.", + action: "Faça o login.", + }); + }); + + test("Com usuário logado porém sem `tasks` no banco", async () => { + newUser = await createUser(); + session = await createSession(newUser); + + const response = await fetch(API.task, { + headers: { + Cookie: `session=${session.id}`, + }, + }); + + expect(response.ok).toBe(true); + expect(response.status).toBe(204); + }); + + test("Com usuário logado e uma `task` no banco", async () => { + const task = { + title: "Teste um", + description: "Testa se a API devolverá essa task", + }; + newTask = await createTask(task, newUser); + + const response = await fetch(API.task, { + headers: { + Cookie: `session=${session.id}`, + }, + }); + + expect(response.ok).toBe(true); + expect(response.status).toBe(200); + + const responseBody = await response.json(); + expect(Array.isArray(responseBody.data)).toBe(true); + expect(responseBody.data[0]).toEqual({ + ...newTask, + created_at: newTask.created_at.toISOString(), + updated_at: newTask.updated_at.toISOString(), + }); + expect(responseBody.message).toContain("1"); + }); +}); + +describe("GET `/tasks/:id`", () => { + test("Sem usuário logado", async () => { + const response = await fetch(`${API.task}/${newTask.id}`); + expect(response.ok).toBe(false); + expect(response.status).toBe(401); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + action: "Faça o login.", + message: "Não autorizado.", + }); + }); + + test("Com usuário logado", async () => { + const response = await fetch(`${API.task}/${newTask.id}`, { + headers: { + Cookie: `session=${session.id}`, + }, + }); + expect(response.ok).toBe(true); + expect(response.status).toBe(200); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + task: { + ...newTask, + created_at: newTask.created_at.toISOString(), + updated_at: newTask.updated_at.toISOString(), + }, + }); + }); + + test("Com um usuário tentando buscar de outro usuário", async () => { + const otherUser = await createUser(); + const otherSession = await createSession(otherUser); + + const response = await fetch(`${API.task}/${newTask.id}`, { + headers: { + Cookie: `session=${otherSession.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(403); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + message: "Voce não tem permissão executar essa ação.", + }); + }); +}); diff --git a/tests/controller/task/patch.test.ts b/tests/controller/task/patch.test.ts new file mode 100644 index 0000000..dc7f0f3 --- /dev/null +++ b/tests/controller/task/patch.test.ts @@ -0,0 +1,182 @@ +import { + API, + createSession, + createTask, + createUser, + waitForAllServices, +} from "$/orchestrator.js"; +import { AuthenticatedUser, UserSession } from "~/models/session.js"; + +beforeAll(async () => { + await waitForAllServices(); +}); + +describe("PATCH `/tasks/:id`", () => { + test("Sem usuário logado", async () => { + const fakeTaskId = crypto.randomUUID(); + + const taskForUpdate = { + title: "Atualização da task sem usuário", + description: "O sistema deve bloquear essa ação!", + }; + + const response = await fetch(`${API.task}/${fakeTaskId}`, { + method: "PATCH", + body: JSON.stringify(taskForUpdate), + headers: { + "Content-Type": "application/json", + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(401); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + message: "Não autorizado.", + action: "Faça o login.", + }); + }); + + let defaultUser: AuthenticatedUser; + let defaultSession: UserSession; + test("Com usuário logado porém `task` com `ID` inválido", async () => { + defaultUser = await createUser(); + defaultSession = await createSession(defaultUser); + const invalidId = "id-no-formato-invalido"; + + const taskForUpdate = { + title: "Atualização com ID inválido", + description: "O sistema deve bloquear essa ação!", + }; + + const response = await fetch(`${API.task}/${invalidId}`, { + method: "PATCH", + body: JSON.stringify(taskForUpdate), + headers: { + "Content-Type": "application/json", + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + error: "Id da task deve ser do tipo UUID.", + }); + }); + + test("Com usuário logado porém `task` com `ID` inexistente no banco", async () => { + const fakeTaskId = crypto.randomUUID(); + + const taskForUpdate = { + title: "Atualização da task sem usuário", + description: "O sistema deve bloquear essa ação!", + }; + + const response = await fetch(`${API.task}/${fakeTaskId}`, { + method: "PATCH", + body: JSON.stringify(taskForUpdate), + headers: { + "Content-Type": "application/json", + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(true); + expect(response.status).toBe(204); + }); + + test("Com usuário logado e `task` com `ID` existente no banco", async () => { + const task = { + title: "Atualização da task", + description: "O sistema deve aceitar essa ação!", + }; + const taskForUpdate = await createTask(task, defaultUser); + + const response = await fetch(`${API.task}/${taskForUpdate.id}`, { + method: "PATCH", + body: JSON.stringify({ + title: "Task atualizada!", + description: "O sistema aceitou a atualização!", + status: "done", + }), + headers: { + "Content-Type": "application/json", + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(true); + expect(response.status).toBe(201); + + const responseBody = await response.json(); + expect(responseBody.data.title).not.toBe(task.title); + expect(responseBody.data.description).not.toBe(task.description); + expect(new Date(responseBody.data.updated_at).getTime()).toBeGreaterThan( + new Date(responseBody.data.created_at).getTime(), + ); + }); + + test("Com usuário logado e `task` com `status` inexistente", async () => { + const task = { + title: "Atualização da task", + description: "O sistema deve aceitar essa ação!", + }; + + const taskForUpdate = await createTask(task, defaultUser); + + const response = await fetch(`${API.task}/${taskForUpdate.id}`, { + method: "PATCH", + body: JSON.stringify({ + status: "este_status_nao_existe", + }), + headers: { + "Content-Type": "application/json", + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + error: 'Opção inválida: esperada uma das "pending"|"in_progress"|"done"', + }); + }); + + test("Com um usuário tentando alterar `task` de outro usuário", async () => { + const task = { + title: "Criação da task", + description: "O sistema deve aceitar essa ação!", + }; + + const taskForUpdate = await createTask(task, defaultUser); + + const otherUser = await createUser(); + const otherSession = await createSession(otherUser); + + const response = await fetch(`${API.task}/${taskForUpdate.id}`, { + method: "PATCH", + body: JSON.stringify({ + title: "Alterado por outro usuário?", + description: "O sistema não pode aceitar esta ação!", + }), + headers: { + "Content-Type": "application/json", + Cookie: `session=${otherSession.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(403); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + message: "Voce não tem permissão executar essa ação.", + }); + }); +}); diff --git a/tests/controller/task/post.test.ts b/tests/controller/task/post.test.ts new file mode 100644 index 0000000..7b3f49b --- /dev/null +++ b/tests/controller/task/post.test.ts @@ -0,0 +1,163 @@ +import { + API, + createSession, + createUser, + waitForAllServices, +} from "$/orchestrator.js"; +import { AuthenticatedUser, UserSession } from "~/models/session.js"; + +beforeAll(async () => { + await waitForAllServices(); +}); + +describe("POST `/tasks`", () => { + test("Sem usuário logado", async () => { + const response = await fetch(API.task, { + method: "POST", + body: JSON.stringify({ + title: "Usuário sem sessão", + description: "O sistema deve bloquear esta ação!", + }), + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(401); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + message: "Não autorizado.", + action: "Faça o login.", + }); + }); + + let defaultUser: AuthenticatedUser; + let defaultSession: UserSession; + test("Com usuário logado e todos os dados corretos", async () => { + const newTask = { + title: "Task com usuário logado", + description: "O sistema deve aceitar esta ação!", + status: "in_progress", + }; + + defaultUser = await createUser(); + defaultSession = await createSession(defaultUser); + + const response = await fetch(API.task, { + method: "POST", + body: JSON.stringify(newTask), + headers: { + "Content-Type": "application/json", + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(true); + expect(response.status).toBe(201); + + const responseBody = await response.json(); + expect(responseBody).toHaveProperty("message"); + expect(responseBody.data).toHaveProperty( + "description", + newTask.description, + ); + expect(responseBody.data).toHaveProperty("title", newTask.title); + expect(responseBody.data).toHaveProperty("id"); + expect(responseBody.data).toHaveProperty("created_at"); + expect(responseBody.data).toHaveProperty("updated_at"); + expect(responseBody.data).toHaveProperty("status"); + expect(responseBody.data.user_id).toBe(defaultUser.id); + }); + + test("Com usuário logado e sem a propriedade `title`", async () => { + const newTaskWithNoTitle = { + description: "O sistema deve negar esta ação!", + }; + + const response = await fetch(API.task, { + method: "POST", + body: JSON.stringify(newTaskWithNoTitle), + headers: { + "Content-Type": "application/json", + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toHaveProperty("error"); + + expect(responseBody.error).toBe("Coluna 'title' é obrigatório!"); + }); + + test("Com usuário logado e sem a propriedade `description`", async () => { + const newTaskWithNoDescription = { + title: "Bad request!", + }; + + const response = await fetch(API.task, { + method: "POST", + body: JSON.stringify(newTaskWithNoDescription), + headers: { + "Content-Type": "application/json", + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toHaveProperty("error"); + + expect(responseBody.error).toBe("Coluna 'description' é obrigatória!"); + }); + + test("Com usuário logado e sem as propriedades `title` e `description`", async () => { + const emptyObject = {}; + + const response = await fetch(API.task, { + method: "POST", + body: JSON.stringify(emptyObject), + headers: { + "Content-Type": "application/json", + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toHaveProperty("error"); + + expect(responseBody.error).toBe( + "Coluna 'title' é obrigatório! | Coluna 'description' é obrigatória!", + ); + }); + + test("Com usuário logado e com `status` inválido", async () => { + const newTaskWithInvalidStatus = { + title: "Tarefa com status invalido", + description: "O sistema deve obrigatóriamente bloquear esta ação.", + status: "status_inválido", + }; + const response = await fetch(API.task, { + method: "POST", + body: JSON.stringify(newTaskWithInvalidStatus), + headers: { + "Content-Type": "application/json", + Cookie: `session=${defaultSession.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + error: 'Opção inválida: esperada uma das "pending"|"in_progress"|"done"', + }); + }); +}); diff --git a/tests/controller/user/get.test.ts b/tests/controller/user/get.test.ts new file mode 100644 index 0000000..efbea10 --- /dev/null +++ b/tests/controller/user/get.test.ts @@ -0,0 +1,122 @@ +import { + API, + createSession, + createUser, + waitForAllServices, +} from "$/orchestrator.js"; + +import { env } from "~/config/env.js"; + +beforeAll(async () => { + await waitForAllServices(); +}); + +describe("GET `/user`", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + test("Sem sessão vinculada", async () => { + const response = await fetch(API.user); + expect(response.ok).toBe(false); + expect(response.status).toBe(401); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + message: "Não autorizado.", + action: "Verifique se está logado e tente novamente.", + }); + }); + + test("Com Sessão vinculada", async () => { + const newUser = await createUser(); + const session = await createSession(newUser); + const response = await fetch(API.user, { + headers: { + Cookie: `session=${session.id}`, + }, + }); + + expect(response.ok).toBe(true); + expect(response.status).toBe(200); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + user: { + id: newUser.id, + name: newUser.name, + email: newUser.email, + created_at: newUser.created_at.toISOString(), + updated_at: newUser.updated_at.toISOString(), + }, + }); + }); + + test("Com sessão quase expirada", async () => { + const horaAtual = new Date(); + const horaSessaoExpirada = new Date( + horaAtual.getTime() - env.INACTIVITY_TIMEOUT / 2, + ); + vi.setSystemTime(horaSessaoExpirada); + + const newUser = await createUser(); + const session = await createSession(newUser); + + vi.useRealTimers(); + const response = await fetch(API.user, { + headers: { + Cookie: `session=${session.id}`, + }, + }); + + expect(response.ok).toBe(true); + expect(response.status).toBe(200); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + user: { + id: newUser.id, + name: newUser.name, + email: newUser.email, + created_at: newUser.created_at.toISOString(), + updated_at: newUser.updated_at.toISOString(), + }, + }); + + expect(new Date(session.expires_at).getTime()).greaterThan( + horaAtual.getTime(), + ); + expect(session.created_at.getTime()).toBeLessThan(horaAtual.getTime()); + }); + + test("Com sessão expirada", async () => { + const horaAtual = new Date(); + const horaSessaoExpirada = new Date( + horaAtual.getTime() - env.INACTIVITY_TIMEOUT, + ); + vi.setSystemTime(horaSessaoExpirada); + + const newUser = await createUser(); + const session = await createSession(newUser); + + vi.useRealTimers(); + const response = await fetch(API.user, { + headers: { + Cookie: `session=${session.id}`, + }, + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(401); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + message: "Sessão inválida ou expirada.", + action: "Por favor, realize o login novamente.", + }); + }); +}); diff --git a/tests/controller/user/post.test.ts b/tests/controller/user/post.test.ts new file mode 100644 index 0000000..958a9a9 --- /dev/null +++ b/tests/controller/user/post.test.ts @@ -0,0 +1,173 @@ +import { API, clearDatabase, waitForAllServices } from "$/orchestrator.js"; +import crypto from "node:crypto"; + +beforeAll(async () => { + await clearDatabase(); + await waitForAllServices(); +}); + +describe("POST `/user`", () => { + test("Com todos os dados corretos", async () => { + const userData = { + name: "Teste Usuario", + email: "email@teste.com", + password: "senha@teste", + }; + + const response = await fetch(API.user, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + expect(response.ok).toBe(true); + expect(response.status).toBe(201); + const responseBody = await response.json(); + + expect(responseBody).toHaveProperty("message"); + expect(responseBody.message).toContain(userData.name); + }); + + test("Com `email` inválido", async () => { + const userData = { + name: "Email Inválido", + email: "emailnaovalido", + password: "email@invalido", + }; + + const response = await fetch(API.user, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toHaveProperty("error"); + expect(responseBody).toEqual({ error: "Email não enviado ou inválido." }); + }); + + test("Com `nome` excedendo 30 caracteres", async () => { + const userData = { + name: "Nome do Usuario Muito Grande e Inválido ", + email: "nome@invalido.com", + password: "nome-invalido", + }; + + const response = await fetch(API.user, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toHaveProperty("error"); + expect(responseBody).toEqual({ + error: "Nome deve ter no máximo 30 caracteres.", + }); + }); + + test("Com `senha` excedendo 60 caracteres", async () => { + const password = crypto.randomBytes(31).toString("hex"); //gera um string de 62 caracteres + + const userData = { + name: "Com Senha Inválida ", + email: "senha@invalida.com", + password, + }; + + const response = await fetch(API.user, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toHaveProperty("error"); + expect(responseBody).toEqual({ + error: "Senha deve ter no máximo 60 caracteres.", + }); + }); + + test("Sem a propriedade `name`", async () => { + const userData = { + email: "usuario@sem-nome.com", + password: "semNomeDeUsuario", + }; + + const response = await fetch(API.user, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toHaveProperty("error"); + expect(responseBody).toEqual({ error: "Nome é obrigatório." }); + }); + + test("Sem a propriedade `email`", async () => { + const userData = { + name: "Usuario Sem Email", + password: "semNomeDeUsuario", + }; + + const response = await fetch(API.user, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toHaveProperty("error"); + expect(responseBody).toEqual({ error: "Email não enviado ou inválido." }); + }); + + test("Sem a propriedade `password`", async () => { + const userData = { + name: "Usuario Sem Senha", + email: "usuario@sem-senha.com", + }; + + const response = await fetch(API.user, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + expect(response.ok).toBe(false); + expect(response.status).toBe(400); + + const responseBody = await response.json(); + expect(responseBody).toHaveProperty("error"); + expect(responseBody).toEqual({ error: "Senha é obrigátoria." }); + }); +}); diff --git a/tests/orchestrator.ts b/tests/orchestrator.ts new file mode 100644 index 0000000..0f3473c --- /dev/null +++ b/tests/orchestrator.ts @@ -0,0 +1,51 @@ +import retry from "async-retry"; +import { faker } from "@faker-js/faker"; +import { resetDatabase } from "~/infra/scripts/migrator.js"; +import { registerUser } from "~/models/user.js"; +import { UserInput } from "~/zod/userSchema.js"; +import * as session from "~/models/session.js"; +import { taskModel } from "~/models/task.js"; +import { Task } from "~/zod/taskSchema.js"; + +const apiBaseUrl = "http://localhost:5001"; + +export const API = { + user: `${apiBaseUrl}/user`, + login: `${apiBaseUrl}/login`, + task: `${apiBaseUrl}/tasks`, +}; + +export async function waitForAllServices() { + await waitForWebServer(); + + async function waitForWebServer() { + return retry(fetchIndexPage, { + retries: 100, + maxTimeout: 1000, + }); + } + async function fetchIndexPage() { + const response = await fetch(apiBaseUrl); + if (response.status !== 200) throw Error(); + } +} + +export async function clearDatabase() { + await resetDatabase(); +} + +export async function createUser(userValues?: UserInput) { + return await registerUser({ + name: userValues?.name ?? faker.person.fullName(), + email: userValues?.email ?? faker.internet.email(), + password: userValues?.password ?? "ValidPassword", + }); +} + +export async function createSession(user: session.AuthenticatedUser) { + return await session.createSession(user.id); +} + +export async function createTask(task: Task, user: session.AuthenticatedUser) { + return await taskModel.create(task, user.id); +} From 4cb815b1b3ce4614771cb217c30728b8f6f891ab Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 18:07:29 -0300 Subject: [PATCH 07/14] =?UTF-8?q?chore:=20implementa=20altera=C3=A7=C3=B5e?= =?UTF-8?q?s=20no=20readme=20e=20na=20pagina=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++++++---- src/controller/index.ts | 22 ++++++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7190559..9160fc9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # API-REST-Node.js-TypeScript +- API node.js para manipulação de tarefas com cobertura de 100% de testes automatizados. + ## 🚀 Tecnologias ### Principais @@ -17,6 +19,11 @@ - **CORS** (controle de acesso entre dominios) - **Docker** (com PostgreSQL para desenvolvimento local) +### Testes Automatizados + +- **Vitest** (criação dos testes) +- **Faker-js** (Mock de dados) + --- ## 🗂️ Estrutura de Entidades @@ -27,13 +34,14 @@ - `name` - `email` (único) - `password` (hash) -- `createdAt` -- `updatedAt` +- `created_at` +- `updated_at` ### UserSession - `id` (uuid) - `user_id` +- `created_at` - `expires_at` - `last_active_at` @@ -44,8 +52,8 @@ - `description` - `status` (`TaskStatus`) - `userId` (`FK → User`) -- `createdAt` -- `updatedAt` +- `created_at` +- `updated_at` ### Enums diff --git a/src/controller/index.ts b/src/controller/index.ts index 8113fb0..e064035 100644 --- a/src/controller/index.ts +++ b/src/controller/index.ts @@ -32,14 +32,22 @@ export const index = (req: Request, res: Response) => {
  • Node.js + Express
  • TypeScript
  • -
  • PostgreSQL (Prisma)
  • +
  • PostgreSQL (com Prisma)
  • Bcrypt (hash de senhas)
  • Zod (validação)
  • CookieParser (sessões HttpOnly)
  • -
  • Dotenv + CORS
+
+

🧰 Auxiliares

+
    +
  • Dotenv + Dotenv Expand
  • +
  • CORS
  • +
  • Docker (banco de dados local)
  • +
+
+

@@ -53,7 +61,8 @@ export const index = (req: Request, res: Response) => {
  • name
  • email (único)
  • password (hash)
  • -
  • createdAt
  • +
  • created_at
  • +
  • updated_at
  • @@ -64,7 +73,7 @@ export const index = (req: Request, res: Response) => {
  • description
  • status TaskStatus
  • userId (FK → User)
  • -
  • createdAt / updatedAt
  • +
  • created_at / updatedAt
  • @@ -84,13 +93,14 @@ export const index = (req: Request, res: Response) => {

    Auth

      -
    • POST /register
    • +
    • GET /user
    • +
    • POST /user
    • POST /login
    • -
    • GET /validate

    Tasks

    • GET /tasks
    • +
    • GET /tasks/:id
    • POST /tasks
    • PUT /tasks/:id
    • DELETE /tasks/:id
    • From 0a4591c03e9d6081de90be781d135dd090e92813 Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 18:10:35 -0300 Subject: [PATCH 08/14] feat: implementa CI para testes automatizados --- .github/workflows/tests.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..2d0730f --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,15 @@ +name: Testes Automatizados + +on: pull_request + +jobs: + vitest: + name: Vitest Ubuntu + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: "package.json" + - run: npm ci + - run: npm test From 3916bb02f0c1de00215a8f9e048b8644c0626e80 Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 18:17:03 -0300 Subject: [PATCH 09/14] =?UTF-8?q?ci:=20corrige=20execu=C3=A7=C3=A3o=20do?= =?UTF-8?q?=20prisma=20generate=20apos=20instalar=20libs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tests.yaml | 26 ++++++++++++++++++++++++-- package.json | 7 +++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2d0730f..67507c9 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -8,8 +8,30 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version-file: "package.json" - - run: npm ci - - run: npm test + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Setup CI Environment + run: | + touch .env + echo "DATABASE_URL=postgresql://local_user:local_password@localhost:5432/local_db?schema=public" >> .env + + - name: Run Infrastructure & Tests + run: | + docker compose -f src/infra/compose.yaml up -d + npx prisma migrate deploy + npx prisma generate + npx vitest run + env: + POSTGRES_USER: local_user + POSTGRES_PASSWORD: local_password + POSTGRES_DB: local_db + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + DATABASE_URL: postgresql://local_user:local_password@localhost:5432/local_db?schema=public diff --git a/package.json b/package.json index 1ed9f85..1a04aa4 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,9 @@ "migrations:reset": "dotenv -e .env -- prisma migrate reset --force", "migrations:generate": "prisma generate", "server:watch": "tsx --watch src/server.ts", - "start": "node ./dist/server.js", - "test": "npm run services:up && npm run migrations:up && npm run migrations:generate && concurrently -k -s first -n \"server,test\" --hide server \"tsx src/server.ts\" \"vitest", - "test:watch": "vitest", - "postinstall": "npm run migrations:generate" + "start": "npm run migrations:generate && node ./dist/server.js", + "test": "npm run services:up && npm run migrations:up && npm run migrations:generate && concurrently -k -s first -n \"server,test\" --hide server \"tsx src/server.ts\" vitest", + "test:watch": "vitest" }, "repository": { "type": "git", From 907f174e45ce4d9d587322dd3ba01fede7affef0 Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 19:28:13 -0300 Subject: [PATCH 10/14] ci: debug --- .github/workflows/tests.yaml | 2 +- src/infra/scripts/wait-for-postgres.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 67507c9..a13f235 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -12,7 +12,6 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: "package.json" - cache: "npm" - name: Install dependencies run: npm ci @@ -25,6 +24,7 @@ jobs: - name: Run Infrastructure & Tests run: | docker compose -f src/infra/compose.yaml up -d + npx tsx src/infra/scripts/wait-for-postgres.ts npx prisma migrate deploy npx prisma generate npx vitest run diff --git a/src/infra/scripts/wait-for-postgres.ts b/src/infra/scripts/wait-for-postgres.ts index ff1cf6b..4b0b338 100644 --- a/src/infra/scripts/wait-for-postgres.ts +++ b/src/infra/scripts/wait-for-postgres.ts @@ -11,6 +11,7 @@ function checkPostgres() { console.log("\nPostgres pronto!\n"); } } +console.log('wait-for-postgres: Iniciou o CI'); process.stdout.write("\n\nAguardando Postgres"); From 08eddb8f05b7b90421d624c7cca66c9926564671 Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 19:34:06 -0300 Subject: [PATCH 11/14] ci: modifica script q aguarda o postgres --- .github/workflows/tests.yaml | 9 ++------ src/infra/scripts/wait-for-postgres.ts | 30 ++++++++++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a13f235..b6fac33 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -24,14 +24,9 @@ jobs: - name: Run Infrastructure & Tests run: | docker compose -f src/infra/compose.yaml up -d + docker ps -a + docker logs postgres-task npx tsx src/infra/scripts/wait-for-postgres.ts npx prisma migrate deploy npx prisma generate npx vitest run - env: - POSTGRES_USER: local_user - POSTGRES_PASSWORD: local_password - POSTGRES_DB: local_db - POSTGRES_HOST: localhost - POSTGRES_PORT: 5432 - DATABASE_URL: postgresql://local_user:local_password@localhost:5432/local_db?schema=public diff --git a/src/infra/scripts/wait-for-postgres.ts b/src/infra/scripts/wait-for-postgres.ts index 4b0b338..11791bb 100644 --- a/src/infra/scripts/wait-for-postgres.ts +++ b/src/infra/scripts/wait-for-postgres.ts @@ -1,18 +1,26 @@ import { exec } from "node:child_process"; +let attempts = 0; +const MAX_ATTEMPTS = 30; + function checkPostgres() { - exec("docker exec postgres-task pg_isready --host localhost", handleReturn); - function handleReturn(error: any, stdout: any) { - if (stdout.search("accepting connections") === -1) { - process.stdout.write("."); - checkPostgres(); - return; + exec("docker exec postgres-task pg_isready", (error, stdout) => { + attempts++; + + if (stdout.includes("accepting connections")) { + console.log("\n✅ Postgres pronto para conexões!"); + process.exit(0); } - console.log("\nPostgres pronto!\n"); - } -} -console.log('wait-for-postgres: Iniciou o CI'); -process.stdout.write("\n\nAguardando Postgres"); + if (attempts >= MAX_ATTEMPTS) { + console.error("\n❌ Erro: O Postgres não ficou pronto a tempo."); + process.exit(1); + } + + process.stdout.write("."); + setTimeout(checkPostgres, 1000); + }); +} +process.stdout.write("\n⏳ Aguardando Postgres"); checkPostgres(); From c2e47d7c2df6645c976ab8bcfe57c8bba0b34670 Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 19:46:01 -0300 Subject: [PATCH 12/14] ci: modifica container para rodar no ci --- src/infra/compose.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/infra/compose.yaml b/src/infra/compose.yaml index d43d72a..99442a7 100644 --- a/src/infra/compose.yaml +++ b/src/infra/compose.yaml @@ -1,8 +1,10 @@ services: database-task: container_name: "postgres-task" - image: "postgres:18-alpine3.22" - env_file: - - ../../.env + image: "postgres:17-alpine" + environment: + - POSTGRES_PASSWORD=local_password + - POSTGRES_USER=local_user + - POSTGRES_DB=local_db ports: - "5432:5432" From 93a76d8273f6f3e05383f035376c3f297546b63d Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 19:48:58 -0300 Subject: [PATCH 13/14] ci: corrige workflow de test --- .github/workflows/tests.yaml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b6fac33..76874a1 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -22,11 +22,4 @@ jobs: echo "DATABASE_URL=postgresql://local_user:local_password@localhost:5432/local_db?schema=public" >> .env - name: Run Infrastructure & Tests - run: | - docker compose -f src/infra/compose.yaml up -d - docker ps -a - docker logs postgres-task - npx tsx src/infra/scripts/wait-for-postgres.ts - npx prisma migrate deploy - npx prisma generate - npx vitest run + run: npm test From 42f2802c65495206845f4a493e54ab9b9ce1cc24 Mon Sep 17 00:00:00 2001 From: Pedro Barreto Date: Fri, 17 Apr 2026 20:07:18 -0300 Subject: [PATCH 14/14] ci: remove reset no banco de dados --- tests/controller/user/post.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/controller/user/post.test.ts b/tests/controller/user/post.test.ts index 958a9a9..a7dff01 100644 --- a/tests/controller/user/post.test.ts +++ b/tests/controller/user/post.test.ts @@ -2,7 +2,6 @@ import { API, clearDatabase, waitForAllServices } from "$/orchestrator.js"; import crypto from "node:crypto"; beforeAll(async () => { - await clearDatabase(); await waitForAllServices(); });