diff --git a/.husky/pre-commit b/.husky/pre-commit index 0312b76..d0a7784 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - npx lint-staged \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push index 21b8da5..daac54a 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,5 +1,2 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npm run build npm run test \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 58bbe7b..8fb6b87 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,6 +21,16 @@ "internalConsoleOptions": "neverOpen", "skipFiles": ["/**", "${workspaceFolder}/node_modules/**"], "preLaunchTask": "npm: build" + }, + { + "name": "Run Tests", + "type": "node", + "request": "launch", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "test"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "skipFiles": ["/**", "${workspaceFolder}/node_modules/**"] } ] } diff --git a/biome.json b/biome.json index 680f4ee..a19c59f 100644 --- a/biome.json +++ b/biome.json @@ -21,5 +21,10 @@ "noForEach": "off" } } + }, + "javascript": { + "parser": { + "unsafeParameterDecoratorsEnabled": true + } } } diff --git a/package-lock.json b/package-lock.json index 2fa54fd..ed1e4ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.14", "license": "MIT", "dependencies": { - "@asteasolutions/zod-to-openapi": "^7.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "envalid": "^8.0.0", @@ -20,7 +19,7 @@ "pino": "^9.4.0", "pino-http": "^10.0.0", "swagger-ui-express": "^5.0.0", - "zod": "^3.22.4" + "tsoa": "^6.4.0" }, "devDependencies": { "@biomejs/biome": "1.9.3", @@ -31,7 +30,6 @@ "husky": "^9.0.11", "lint-staged": "^15.2.2", "pino-pretty": "^11.0.0", - "rimraf": "^6.0.0", "supertest": "^7.0.0", "tsup": "^8.0.2", "tsx": "^4.7.2", @@ -40,18 +38,6 @@ "vitest": "^2.0.0" } }, - "node_modules/@asteasolutions/zod-to-openapi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.1.2.tgz", - "integrity": "sha512-tuDcV4aGAlY4eaZ8Qmf1efPL33hwJKdpCSbI6vJqXU5Wkz9IIyCrb3u3fExZyyMGzmLKcJH+CHI5UKvNBPlyjg==", - "license": "MIT", - "dependencies": { - "openapi3-ts": "^4.1.2" - }, - "peerDependencies": { - "zod": "^3.20.2" - } - }, "node_modules/@biomejs/biome": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.3.tgz", @@ -624,11 +610,326 @@ "node": ">=18" } }, + "node_modules/@hapi/accept": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz", + "integrity": "sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/ammo": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-6.0.1.tgz", + "integrity": "sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/b64": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz", + "integrity": "sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/boom": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-3.0.1.tgz", + "integrity": "sha512-G+/Pp9c1Ha4FDP+3Sy/Xwg2O4Ahaw3lIZFSX+BL4uWi64CmiETuZPxhKDUD4xBMOUZbBlzvO8HjiK8ePnhBadA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/call": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@hapi/call/-/call-9.0.1.tgz", + "integrity": "sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/catbox": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-12.1.1.tgz", + "integrity": "sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/podium": "^5.0.0", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/catbox-memory": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-6.0.2.tgz", + "integrity": "sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/content": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@hapi/content/-/content-6.0.0.tgz", + "integrity": "sha512-CEhs7j+H0iQffKfe5Htdak5LBOz/Qc8TRh51cF+BFv0qnuph3Em4pjGVzJMkI2gfTDdlJKWJISGWS1rK34POGA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.0" + } + }, + "node_modules/@hapi/cryptiles": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-6.0.1.tgz", + "integrity": "sha512-9GM9ECEHfR8lk5ASOKG4+4ZsEzFqLfhiryIJ2ISePVB92OHLp/yne4m+zn7z9dgvM98TLpiFebjDFQ0UHcqxXQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/file": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/file/-/file-3.0.0.tgz", + "integrity": "sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hapi": { + "version": "21.3.10", + "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-21.3.10.tgz", + "integrity": "sha512-CmEcmTREW394MaGGKvWpoOK4rG8tKlpZLs30tbaBzhCrhiL2Ti/HARek9w+8Ya4nMBGcd+kDAzvU44OX8Ms0Jg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/accept": "^6.0.1", + "@hapi/ammo": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.1", + "@hapi/call": "^9.0.1", + "@hapi/catbox": "^12.1.1", + "@hapi/catbox-memory": "^6.0.2", + "@hapi/heavy": "^8.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/mimos": "^7.0.1", + "@hapi/podium": "^5.0.1", + "@hapi/shot": "^6.0.1", + "@hapi/somever": "^4.1.1", + "@hapi/statehood": "^8.1.1", + "@hapi/subtext": "^8.1.0", + "@hapi/teamwork": "^6.0.0", + "@hapi/topo": "^6.0.1", + "@hapi/validate": "^2.0.1" + }, + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/@hapi/heavy": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-8.0.1.tgz", + "integrity": "sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/hoek": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.4.tgz", + "integrity": "sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/iron": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-7.0.1.tgz", + "integrity": "sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/mimos": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-7.0.1.tgz", + "integrity": "sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "mime-db": "^1.52.0" + } + }, + "node_modules/@hapi/nigel": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-5.0.1.tgz", + "integrity": "sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/vise": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/pez": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-6.1.0.tgz", + "integrity": "sha512-+FE3sFPYuXCpuVeHQ/Qag1b45clR2o54QoonE/gKHv9gukxQ8oJJZPR7o3/ydDTK6racnCJXxOyT1T93FCJMIg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/content": "^6.0.0", + "@hapi/hoek": "^11.0.2", + "@hapi/nigel": "^5.0.1" + } + }, + "node_modules/@hapi/podium": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-5.0.1.tgz", + "integrity": "sha512-eznFTw6rdBhAijXFIlBOMJJd+lXTvqbrBIS4Iu80r2KTVIo4g+7fLy4NKp/8+UnSt5Ox6mJtAlKBU/Sf5080TQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/teamwork": "^6.0.0", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/shot": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-6.0.1.tgz", + "integrity": "sha512-s5ynMKZXYoDd3dqPw5YTvOR/vjHvMTxc388+0qL0jZZP1+uwXuUD32o9DuuuLsmTlyXCWi02BJl1pBpwRuUrNA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/somever": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-4.1.1.tgz", + "integrity": "sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/bounce": "^3.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/statehood": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-8.1.1.tgz", + "integrity": "sha512-YbK7PSVUA59NArAW5Np0tKRoIZ5VNYUicOk7uJmWZF6XyH5gGL+k62w77SIJb0AoAJ0QdGQMCQ/WOGL1S3Ydow==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/iron": "^7.0.1", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/subtext": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-8.1.0.tgz", + "integrity": "sha512-PyaN4oSMtqPjjVxLny1k0iYg4+fwGusIhaom9B2StinBclHs7v46mIW706Y+Wo21lcgulGyXbQrmT/w4dus6ww==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/content": "^6.0.0", + "@hapi/file": "^3.0.0", + "@hapi/hoek": "^11.0.2", + "@hapi/pez": "^6.1.0", + "@hapi/wreck": "^18.0.1" + } + }, + "node_modules/@hapi/teamwork": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-6.0.0.tgz", + "integrity": "sha512-05HumSy3LWfXpmJ9cr6HzwhAavrHkJ1ZRCmNE2qJMihdM5YcWreWPfyN0yKT2ZjCM92au3ZkuodjBxOibxM67A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/validate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", + "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/topo": "^6.0.1" + } + }, + "node_modules/@hapi/vise": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-5.0.1.tgz", + "integrity": "sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/wreck": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.1.0.tgz", + "integrity": "sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -646,14 +947,12 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -671,7 +970,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -742,7 +1040,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -973,11 +1270,65 @@ "win32" ] }, + "node_modules/@tsoa/cli": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@tsoa/cli/-/cli-6.4.0.tgz", + "integrity": "sha512-cuKqIAJ33enhXejMb04P2JF/SZdM3WLFE9Qg8s2dOVOrLbbZGK3H85YSORif0unxbre02psXuUyl3FwWBt0/pQ==", + "license": "MIT", + "dependencies": { + "@tsoa/runtime": "^6.4.0", + "@types/multer": "^1.4.11", + "fs-extra": "^11.2.0", + "glob": "^10.3.10", + "handlebars": "^4.7.8", + "merge-anything": "^5.1.4", + "minimatch": "^9.0.1", + "ts-deepmerge": "^7.0.0", + "typescript": "^5.3.3", + "validator": "^13.11.0", + "yaml": "^2.4.1", + "yargs": "^17.7.1" + }, + "bin": { + "tsoa": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/@tsoa/runtime": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@tsoa/runtime/-/runtime-6.4.0.tgz", + "integrity": "sha512-oiVVK++Svo8KMRVTfOQxtepPQRCpPh8bCmPpmNFePGZYJAtZ8XX1cm8Zs/v87dZZmNEooU6egj1G40ive/9phw==", + "license": "MIT", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hapi": "^21.3.3", + "@types/koa": "^2.15.0", + "@types/multer": "^1.4.11", + "express": "^4.18.3", + "reflect-metadata": "^0.2.1", + "validator": "^13.11.0" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -988,12 +1339,17 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/content-disposition": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", + "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", + "license": "MIT" + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -1001,6 +1357,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cookies": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", + "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", @@ -1022,7 +1390,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -1035,7 +1402,6 @@ "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -1044,13 +1410,49 @@ "@types/send": "*" } }, + "node_modules/@types/http-assert": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.5.tgz", + "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==", + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, "license": "MIT" }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "license": "MIT" + }, + "node_modules/@types/koa": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", + "license": "MIT", + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", + "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -1062,14 +1464,21 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.7.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -1079,21 +1488,18 @@ "version": "6.9.16", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -1104,7 +1510,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -1306,7 +1711,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1319,7 +1723,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1392,7 +1795,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -1456,7 +1858,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -1651,11 +2052,106 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "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==", + "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", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1668,7 +2164,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/colorette": { @@ -1781,7 +2276,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1894,7 +2388,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/ee-first": { @@ -2015,6 +2508,15 @@ "@esbuild/win32-x64": "0.23.1" } }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2204,7 +2706,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -2265,6 +2766,20 @@ "node": ">= 0.6" } }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2367,25 +2882,21 @@ } }, "node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": "20 || >=22" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -2403,6 +2914,21 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -2422,6 +2948,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -2663,29 +3225,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, - "node_modules/jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -2696,6 +3253,18 @@ "node": ">=10" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/lilconfig": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", @@ -2867,16 +3436,6 @@ "get-func-name": "^2.0.1" } }, - "node_modules/lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/magic-string": { "version": "0.30.11", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", @@ -2896,6 +3455,21 @@ "node": ">= 0.6" } }, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -2995,16 +3569,15 @@ } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": "20 || >=22" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3014,7 +3587,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3024,7 +3596,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -3076,6 +3647,12 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3183,20 +3760,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi3-ts": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", - "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", - "license": "MIT", - "dependencies": { - "yaml": "^2.5.0" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parseurl": { @@ -3212,29 +3779,33 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "20 || >=22" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", @@ -3581,6 +4152,21 @@ "node": ">= 12.13.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -3641,26 +4227,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", @@ -3820,7 +4386,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3833,7 +4398,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3868,7 +4432,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -4000,7 +4563,6 @@ "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", @@ -4015,7 +4577,6 @@ "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" @@ -4025,14 +4586,12 @@ "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/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4042,7 +4601,6 @@ "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" @@ -4055,7 +4613,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -4072,7 +4629,6 @@ "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" @@ -4085,7 +4641,6 @@ "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" @@ -4150,83 +4705,6 @@ "node": ">= 6" } }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/sucrase/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/superagent": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", @@ -4481,6 +4959,15 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-deepmerge": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.1.tgz", + "integrity": "sha512-JBFCmNenZdUCc+TRNCtXVM6N8y/nDQHAcpj5BlwXG/gnogjam1NunulB9ia68mnqYI446giMfpqeBFFkOleh+g==", + "license": "ISC", + "engines": { + "node": ">=14.13.1" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -4515,6 +5002,23 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "license": "0BSD" }, + "node_modules/tsoa": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tsoa/-/tsoa-6.4.0.tgz", + "integrity": "sha512-bgNJrpkoTRjbFo/3S3fH+wh9mXTbTAJkzfUXgsAFZJNaDr1+tmiEaP3OO4AbjH0i1acKu9HTTmok3tWau4T83Q==", + "license": "MIT", + "dependencies": { + "@tsoa/cli": "^6.4.0", + "@tsoa/runtime": "^6.4.0" + }, + "bin": { + "tsoa": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, "node_modules/tsup": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.3.0.tgz", @@ -4745,7 +5249,6 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -4755,13 +5258,34 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz", + "integrity": "sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4780,6 +5304,15 @@ "node": ">= 0.4.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5484,7 +6017,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -5513,6 +6045,12 @@ "node": ">=8" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", @@ -5536,7 +6074,6 @@ "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", @@ -5554,7 +6091,6 @@ "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" @@ -5564,7 +6100,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5580,14 +6115,12 @@ "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/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5597,7 +6130,6 @@ "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", @@ -5612,7 +6144,6 @@ "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" @@ -5628,6 +6159,15 @@ "dev": true, "license": "ISC" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", @@ -5640,13 +6180,81 @@ "node": ">= 14" } }, - "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "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==", + "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==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } } } diff --git a/package.json b/package.json index e99696f..ebfc0c4 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,14 @@ "author": "Edwin Hernandez", "repository": "edwinhern/express-typescript-2024", "license": "MIT", - "main": "index.ts", + "main": "src/index.ts", "private": true, + "type": "module", "scripts": { - "dev": "tsx watch --clear-screen=false src/index.ts | pino-pretty", - "build": "tsup", + "dev": "tsx watch --ignore 'src/api/generated/**' --clear-screen=false src/index.ts | pino-pretty", + "build-api": "tsx src/common/utils/tsoa-generator.ts", + "build": "tsoa spec-and-routes && tsup", "start": "node dist/index.js", - "clean": "rimraf dist coverage", "lint": "biome check src/", "lint:fix": "biome check src/ --fix", "format": "biome format src/", @@ -19,7 +20,6 @@ "prepare": "husky" }, "dependencies": { - "@asteasolutions/zod-to-openapi": "^7.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "envalid": "^8.0.0", @@ -30,7 +30,7 @@ "pino": "^9.4.0", "pino-http": "^10.0.0", "swagger-ui-express": "^5.0.0", - "zod": "^3.22.4" + "tsoa": "^6.4.0" }, "devDependencies": { "@biomejs/biome": "1.9.3", @@ -41,7 +41,6 @@ "husky": "^9.0.11", "lint-staged": "^15.2.2", "pino-pretty": "^11.0.0", - "rimraf": "^6.0.0", "supertest": "^7.0.0", "tsup": "^8.0.2", "tsx": "^4.7.2", @@ -54,8 +53,10 @@ }, "tsup": { "entry": ["src", "!src/**/__tests__/**", "!src/**/*.test.*"], - "splitting": false, - "sourcemap": true, - "clean": true + "sourcemap": false, + "clean": true, + "minify": true, + "format": ["esm"], + "skipNodeModulesBundle": true } } diff --git a/src/api-docs/__tests__/openAPIRouter.test.ts b/src/api-docs/__tests__/openAPIRouter.test.ts deleted file mode 100644 index 131b293..0000000 --- a/src/api-docs/__tests__/openAPIRouter.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { StatusCodes } from "http-status-codes"; -import request from "supertest"; - -import { app } from "@/server"; - -import { generateOpenAPIDocument } from "../openAPIDocumentGenerator"; - -describe("OpenAPI Router", () => { - describe("Swagger JSON route", () => { - it("should return Swagger JSON content", async () => { - // Arrange - const expectedResponse = generateOpenAPIDocument(); - - // Act - const response = await request(app).get("/swagger.json"); - - // Assert - expect(response.status).toBe(StatusCodes.OK); - expect(response.type).toBe("application/json"); - expect(response.body).toEqual(expectedResponse); - }); - - it("should serve the Swagger UI", async () => { - // Act - const response = await request(app).get("/"); - - // Assert - expect(response.status).toBe(StatusCodes.OK); - expect(response.text).toContain("swagger-ui"); - }); - }); -}); diff --git a/src/api-docs/openAPIDocumentGenerator.ts b/src/api-docs/openAPIDocumentGenerator.ts deleted file mode 100644 index 4c5abfb..0000000 --- a/src/api-docs/openAPIDocumentGenerator.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { OpenAPIRegistry, OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi"; - -import { healthCheckRegistry } from "@/api/healthCheck/healthCheckRouter"; -import { userRegistry } from "@/api/user/userRouter"; - -export function generateOpenAPIDocument() { - const registry = new OpenAPIRegistry([healthCheckRegistry, userRegistry]); - const generator = new OpenApiGeneratorV3(registry.definitions); - - return generator.generateDocument({ - openapi: "3.0.0", - info: { - version: "1.0.0", - title: "Swagger API", - }, - externalDocs: { - description: "View the raw OpenAPI Specification in JSON format", - url: "/swagger.json", - }, - }); -} diff --git a/src/api-docs/openAPIResponseBuilders.ts b/src/api-docs/openAPIResponseBuilders.ts deleted file mode 100644 index f2b0126..0000000 --- a/src/api-docs/openAPIResponseBuilders.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { StatusCodes } from "http-status-codes"; -import type { z } from "zod"; - -import { ServiceResponseSchema } from "@/common/models/serviceResponse"; - -export function createApiResponse(schema: z.ZodTypeAny, description: string, statusCode = StatusCodes.OK) { - return { - [statusCode]: { - description, - content: { - "application/json": { - schema: ServiceResponseSchema(schema), - }, - }, - }, - }; -} - -// Use if you want multiple responses for a single endpoint - -// import { ResponseConfig } from '@asteasolutions/zod-to-openapi'; -// import { ApiResponseConfig } from '@common/models/openAPIResponseConfig'; -// export type ApiResponseConfig = { -// schema: z.ZodTypeAny; -// description: string; -// statusCode: StatusCodes; -// }; -// export function createApiResponses(configs: ApiResponseConfig[]) { -// const responses: { [key: string]: ResponseConfig } = {}; -// configs.forEach(({ schema, description, statusCode }) => { -// responses[statusCode] = { -// description, -// content: { -// 'application/json': { -// schema: ServiceResponseSchema(schema), -// }, -// }, -// }; -// }); -// return responses; -// } diff --git a/src/api-docs/openAPIRouter.ts b/src/api-docs/openAPIRouter.ts deleted file mode 100644 index 0d61442..0000000 --- a/src/api-docs/openAPIRouter.ts +++ /dev/null @@ -1,14 +0,0 @@ -import express, { type Request, type Response, type Router } from "express"; -import swaggerUi from "swagger-ui-express"; - -import { generateOpenAPIDocument } from "@/api-docs/openAPIDocumentGenerator"; - -export const openAPIRouter: Router = express.Router(); -const openAPIDocument = generateOpenAPIDocument(); - -openAPIRouter.get("/swagger.json", (_req: Request, res: Response) => { - res.setHeader("Content-Type", "application/json"); - res.send(openAPIDocument); -}); - -openAPIRouter.use("/", swaggerUi.serve, swaggerUi.setup(openAPIDocument)); diff --git a/src/api/generated/routes.ts b/src/api/generated/routes.ts new file mode 100644 index 0000000..5416f51 --- /dev/null +++ b/src/api/generated/routes.ts @@ -0,0 +1,183 @@ +/* tslint:disable */ +/* eslint-disable */ +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { ExpressTemplateService, type TsoaRoute, fetchMiddlewares } from "@tsoa/runtime"; +import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from "express"; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { HealthCheckController } from "./../healthCheck/healthCheckController.js"; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { UserController } from "./../user/userController.js"; + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + +const models: TsoaRoute.Models = { + IHealthCheckResponse: { + dataType: "refObject", + properties: { + success: { dataType: "boolean", required: true }, + message: { dataType: "string", required: true }, + responseObject: { dataType: "enum", enums: [null], required: true }, + statusCode: { dataType: "double", required: true }, + }, + additionalProperties: false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + User: { + dataType: "refObject", + properties: { + id: { dataType: "double", required: true }, + name: { dataType: "string", required: true }, + email: { dataType: "string", required: true }, + age: { dataType: "double", required: true }, + createdAt: { dataType: "datetime", required: true }, + updatedAt: { dataType: "datetime", required: true }, + }, + additionalProperties: false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + IUsersResponse: { + dataType: "refObject", + properties: { + success: { dataType: "boolean", required: true }, + message: { dataType: "string", required: true }, + responseObject: { + dataType: "union", + subSchemas: [ + { dataType: "array", array: { dataType: "refObject", ref: "User" } }, + { dataType: "enum", enums: [null] }, + ], + required: true, + }, + statusCode: { dataType: "double", required: true }, + }, + additionalProperties: false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + IUserResponse: { + dataType: "refObject", + properties: { + success: { dataType: "boolean", required: true }, + message: { dataType: "string", required: true }, + responseObject: { + dataType: "union", + subSchemas: [{ ref: "User" }, { dataType: "enum", enums: [null] }], + required: true, + }, + statusCode: { dataType: "double", required: true }, + }, + additionalProperties: false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +}; +const templateService = new ExpressTemplateService(models, { + noImplicitAdditionalProperties: "silently-remove-extras", + bodyCoercion: true, +}); + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + +export function RegisterRoutes(app: Router) { + // ########################################################################################################### + // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look + // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa + // ########################################################################################################### + + app.get( + "/api/health-check", + ...fetchMiddlewares(HealthCheckController), + ...fetchMiddlewares(HealthCheckController.prototype.getHealthCheck), + + async function HealthCheckController_getHealthCheck(request: ExRequest, response: ExResponse, next: any) { + const args: Record = {}; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args, request, response }); + + const controller = new HealthCheckController(); + + await templateService.apiHandler({ + methodName: "getHealthCheck", + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }, + ); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get( + "/api/user", + ...fetchMiddlewares(UserController), + ...fetchMiddlewares(UserController.prototype.getUsers), + + async function UserController_getUsers(request: ExRequest, response: ExResponse, next: any) { + const args: Record = {}; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: "getUsers", + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }, + ); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get( + "/api/user/:userId", + ...fetchMiddlewares(UserController), + ...fetchMiddlewares(UserController.prototype.getUser), + + async function UserController_getUser(request: ExRequest, response: ExResponse, next: any) { + const args: Record = { + userId: { in: "path", name: "userId", required: true, dataType: "double" }, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: "getUser", + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }, + ); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +} + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git a/src/api/generated/swagger.json b/src/api/generated/swagger.json new file mode 100644 index 0000000..24c4599 --- /dev/null +++ b/src/api/generated/swagger.json @@ -0,0 +1,201 @@ +{ + "openapi": "3.0.0", + "components": { + "examples": {}, + "headers": {}, + "parameters": {}, + "requestBodies": {}, + "responses": {}, + "schemas": { + "IHealthCheckResponse": { + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "responseObject": { + "type": "number", + "enum": [null], + "nullable": true + }, + "statusCode": { + "type": "number", + "format": "double" + } + }, + "required": ["success", "message", "responseObject", "statusCode"], + "type": "object", + "additionalProperties": false + }, + "User": { + "properties": { + "id": { + "type": "number", + "format": "double" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "age": { + "type": "number", + "format": "double" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["id", "name", "email", "age", "createdAt", "updatedAt"], + "type": "object", + "additionalProperties": false + }, + "IUsersResponse": { + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "responseObject": { + "items": { + "$ref": "#/components/schemas/User" + }, + "type": "array", + "nullable": true + }, + "statusCode": { + "type": "number", + "format": "double" + } + }, + "required": ["success", "message", "responseObject", "statusCode"], + "type": "object", + "additionalProperties": false + }, + "IUserResponse": { + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "responseObject": { + "allOf": [ + { + "$ref": "#/components/schemas/User" + } + ], + "nullable": true + }, + "statusCode": { + "type": "number", + "format": "double" + } + }, + "required": ["success", "message", "responseObject", "statusCode"], + "type": "object", + "additionalProperties": false + } + }, + "securitySchemes": {} + }, + "info": { + "title": "express-typescript-boilerplate", + "version": "1.0.14", + "description": "An Express boilerplate backend", + "license": { + "name": "MIT" + }, + "contact": { + "name": "Edwin Hernandez" + } + }, + "paths": { + "/health-check": { + "get": { + "operationId": "GetHealthCheck", + "responses": { + "200": { + "description": "Service is healthy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IHealthCheckResponse" + } + } + } + } + }, + "tags": ["Health Check"], + "security": [], + "parameters": [] + } + }, + "/user": { + "get": { + "operationId": "GetUsers", + "responses": { + "200": { + "description": "Users found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IUsersResponse" + } + } + } + } + }, + "tags": ["Users"], + "security": [], + "parameters": [] + } + }, + "/user/{userId}": { + "get": { + "operationId": "GetUser", + "responses": { + "200": { + "description": "User found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IUserResponse" + } + } + } + } + }, + "tags": ["Users"], + "security": [], + "parameters": [ + { + "in": "path", + "name": "userId", + "required": true, + "schema": { + "format": "double", + "type": "number" + } + } + ] + } + } + }, + "servers": [ + { + "url": "/api" + } + ] +} diff --git a/src/api/healthCheck/__tests__/healthCheckRouter.test.ts b/src/api/healthCheck/__tests__/healthCheckController.test.ts similarity index 66% rename from src/api/healthCheck/__tests__/healthCheckRouter.test.ts rename to src/api/healthCheck/__tests__/healthCheckController.test.ts index 791d65c..60d4ba7 100644 --- a/src/api/healthCheck/__tests__/healthCheckRouter.test.ts +++ b/src/api/healthCheck/__tests__/healthCheckController.test.ts @@ -1,13 +1,13 @@ import { StatusCodes } from "http-status-codes"; import request from "supertest"; -import type { ServiceResponse } from "@/common/models/serviceResponse"; +import type { IHealthCheckResponse } from "@/api/healthCheck/healthCheckController"; import { app } from "@/server"; describe("Health Check API endpoints", () => { it("GET / - success", async () => { - const response = await request(app).get("/health-check"); - const result: ServiceResponse = response.body; + const response = await request(app).get("/api/health-check"); + const result: IHealthCheckResponse = response.body; expect(response.statusCode).toEqual(StatusCodes.OK); expect(result.success).toBeTruthy(); diff --git a/src/api/healthCheck/healthCheckController.ts b/src/api/healthCheck/healthCheckController.ts new file mode 100644 index 0000000..f1764bb --- /dev/null +++ b/src/api/healthCheck/healthCheckController.ts @@ -0,0 +1,16 @@ +import { Controller, Get, Route, SuccessResponse, Tags } from "@tsoa/runtime"; +import { StatusCodes } from "http-status-codes"; + +import { ServiceResponse } from "@/common/models/serviceResponse"; + +export interface IHealthCheckResponse extends ServiceResponse {} + +@Route("health-check") +@Tags("Health Check") +export class HealthCheckController extends Controller { + @Get() + @SuccessResponse(StatusCodes.OK, "Service is healthy") + public async getHealthCheck(): Promise { + return ServiceResponse.success("Service is healthy", null); + } +} diff --git a/src/api/healthCheck/healthCheckRouter.ts b/src/api/healthCheck/healthCheckRouter.ts deleted file mode 100644 index 66ac17e..0000000 --- a/src/api/healthCheck/healthCheckRouter.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi"; -import express, { type Request, type Response, type Router } from "express"; -import { z } from "zod"; - -import { createApiResponse } from "@/api-docs/openAPIResponseBuilders"; -import { ServiceResponse } from "@/common/models/serviceResponse"; -import { handleServiceResponse } from "@/common/utils/httpHandlers"; - -export const healthCheckRegistry = new OpenAPIRegistry(); -export const healthCheckRouter: Router = express.Router(); - -healthCheckRegistry.registerPath({ - method: "get", - path: "/health-check", - tags: ["Health Check"], - responses: createApiResponse(z.null(), "Success"), -}); - -healthCheckRouter.get("/", (_req: Request, res: Response) => { - const serviceResponse = ServiceResponse.success("Service is healthy", null); - return handleServiceResponse(serviceResponse, res); -}); diff --git a/src/api/user/__tests__/userRouter.test.ts b/src/api/user/__tests__/userController.test.ts similarity index 66% rename from src/api/user/__tests__/userRouter.test.ts rename to src/api/user/__tests__/userController.test.ts index ce2f92a..5c6d787 100644 --- a/src/api/user/__tests__/userRouter.test.ts +++ b/src/api/user/__tests__/userController.test.ts @@ -1,43 +1,43 @@ import { StatusCodes } from "http-status-codes"; import request from "supertest"; -import type { User } from "@/api/user/userModel"; +import type { IUserResponse, IUsersResponse, User } from "@/api/user/userModel"; import { users } from "@/api/user/userRepository"; import type { ServiceResponse } from "@/common/models/serviceResponse"; import { app } from "@/server"; describe("User API Endpoints", () => { - describe("GET /users", () => { + describe("GET /api/user", () => { it("should return a list of users", async () => { // Act - const response = await request(app).get("/users"); - const responseBody: ServiceResponse = response.body; + const response = await request(app).get("/api/user"); + const responseBody: IUsersResponse = response.body; // Assert expect(response.statusCode).toEqual(StatusCodes.OK); expect(responseBody.success).toBeTruthy(); expect(responseBody.message).toContain("Users found"); - expect(responseBody.responseObject.length).toEqual(users.length); - responseBody.responseObject.forEach((user, index) => compareUsers(users[index] as User, user)); + expect(responseBody.responseObject?.length).toEqual(users.length); + responseBody.responseObject?.forEach((user, index) => compareUsers(users[index] as User, user)); }); }); - describe("GET /users/:id", () => { + describe("GET /api/user/:id", () => { it("should return a user for a valid ID", async () => { // Arrange const testId = 1; const expectedUser = users.find((user) => user.id === testId) as User; // Act - const response = await request(app).get(`/users/${testId}`); - const responseBody: ServiceResponse = response.body; + const response = await request(app).get(`/api/user/${testId}`); + const responseBody: IUserResponse = response.body; // Assert expect(response.statusCode).toEqual(StatusCodes.OK); expect(responseBody.success).toBeTruthy(); expect(responseBody.message).toContain("User found"); if (!expectedUser) throw new Error("Invalid test data: expectedUser is undefined"); - compareUsers(expectedUser, responseBody.responseObject); + compareUsers(expectedUser, responseBody.responseObject as User); }); it("should return a not found error for non-existent ID", async () => { @@ -45,12 +45,12 @@ describe("User API Endpoints", () => { const testId = Number.MAX_SAFE_INTEGER; // Act - const response = await request(app).get(`/users/${testId}`); + const response = await request(app).get(`/api/user/${testId}`); const responseBody: ServiceResponse = response.body; // Assert - expect(response.statusCode).toEqual(StatusCodes.NOT_FOUND); - expect(responseBody.success).toBeFalsy(); + expect(response.statusCode).toEqual(StatusCodes.OK); + expect(responseBody.success).toBeTruthy(); expect(responseBody.message).toContain("User not found"); expect(responseBody.responseObject).toBeNull(); }); @@ -58,14 +58,19 @@ describe("User API Endpoints", () => { it("should return a bad request for invalid ID format", async () => { // Act const invalidInput = "abc"; - const response = await request(app).get(`/users/${invalidInput}`); + const response = await request(app).get(`/api/user/${invalidInput}`); const responseBody: ServiceResponse = response.body; // Assert expect(response.statusCode).toEqual(StatusCodes.BAD_REQUEST); expect(responseBody.success).toBeFalsy(); - expect(responseBody.message).toContain("Invalid input"); - expect(responseBody.responseObject).toBeNull(); + expect(responseBody.message).toEqual("Validation Failed"); + expect(responseBody.responseObject).toEqual({ + userId: { + message: "invalid float number", + value: invalidInput, + }, + }); }); }); }); diff --git a/src/api/user/__tests__/userService.test.ts b/src/api/user/__tests__/userService.test.ts index 3b84ca7..085f3f7 100644 --- a/src/api/user/__tests__/userService.test.ts +++ b/src/api/user/__tests__/userService.test.ts @@ -120,8 +120,8 @@ describe("userService", () => { const result = await userServiceInstance.findById(testId); // Assert - expect(result.statusCode).toEqual(StatusCodes.NOT_FOUND); - expect(result.success).toBeFalsy(); + expect(result.statusCode).toEqual(StatusCodes.OK); + expect(result.success).toBeTruthy(); expect(result.message).equals("User not found"); expect(result.responseObject).toBeNull(); }); diff --git a/src/api/user/userController.ts b/src/api/user/userController.ts index f50a325..a37cc43 100644 --- a/src/api/user/userController.ts +++ b/src/api/user/userController.ts @@ -1,19 +1,27 @@ -import type { Request, RequestHandler, Response } from "express"; +import { Controller, Get, Path, Route, SuccessResponse, Tags } from "@tsoa/runtime"; +import { StatusCodes } from "http-status-codes"; -import { userService } from "@/api/user/userService"; -import { handleServiceResponse } from "@/common/utils/httpHandlers"; +import { UserService } from "@/api/user/userService"; -class UserController { - public getUsers: RequestHandler = async (_req: Request, res: Response) => { - const serviceResponse = await userService.findAll(); - return handleServiceResponse(serviceResponse, res); - }; +@Route("user") +@Tags("Users") +export class UserController extends Controller { + private userService: UserService; - public getUser: RequestHandler = async (req: Request, res: Response) => { - const id = Number.parseInt(req.params.id as string, 10); - const serviceResponse = await userService.findById(id); - return handleServiceResponse(serviceResponse, res); - }; -} + constructor(userService = new UserService()) { + super(); + this.userService = userService; + } + + @Get() + @SuccessResponse(StatusCodes.OK, "Users found") + public async getUsers() { + return this.userService.findAll(); + } -export const userController = new UserController(); + @Get("{userId}") + @SuccessResponse(StatusCodes.OK, "User found") + public async getUser(@Path() userId: number) { + return this.userService.findById(userId); + } +} diff --git a/src/api/user/userModel.ts b/src/api/user/userModel.ts index 7163fc0..57fcf6a 100644 --- a/src/api/user/userModel.ts +++ b/src/api/user/userModel.ts @@ -1,21 +1,13 @@ -import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; -import { z } from "zod"; - -import { commonValidations } from "@/common/utils/commonValidation"; - -extendZodWithOpenApi(z); - -export type User = z.infer; -export const UserSchema = z.object({ - id: z.number(), - name: z.string(), - email: z.string().email(), - age: z.number(), - createdAt: z.date(), - updatedAt: z.date(), -}); - -// Input Validation for 'GET users/:id' endpoint -export const GetUserSchema = z.object({ - params: z.object({ id: commonValidations.id }), -}); +import type { ServiceResponse } from "@/common/models/serviceResponse"; + +export interface User { + id: number; + name: string; + email: string; + age: number; + createdAt: Date; + updatedAt: Date; +} + +export interface IUserResponse extends ServiceResponse {} +export interface IUsersResponse extends ServiceResponse {} diff --git a/src/api/user/userRouter.ts b/src/api/user/userRouter.ts deleted file mode 100644 index c14e495..0000000 --- a/src/api/user/userRouter.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi"; -import express, { type Router } from "express"; -import { z } from "zod"; - -import { createApiResponse } from "@/api-docs/openAPIResponseBuilders"; -import { GetUserSchema, UserSchema } from "@/api/user/userModel"; -import { validateRequest } from "@/common/utils/httpHandlers"; -import { userController } from "./userController"; - -export const userRegistry = new OpenAPIRegistry(); -export const userRouter: Router = express.Router(); - -userRegistry.register("User", UserSchema); - -userRegistry.registerPath({ - method: "get", - path: "/users", - tags: ["User"], - responses: createApiResponse(z.array(UserSchema), "Success"), -}); - -userRouter.get("/", userController.getUsers); - -userRegistry.registerPath({ - method: "get", - path: "/users/{id}", - tags: ["User"], - request: { params: GetUserSchema.shape.params }, - responses: createApiResponse(UserSchema, "Success"), -}); - -userRouter.get("/:id", validateRequest(GetUserSchema), userController.getUser); diff --git a/src/api/user/userService.ts b/src/api/user/userService.ts index dfb2491..a1e6bfd 100644 --- a/src/api/user/userService.ts +++ b/src/api/user/userService.ts @@ -1,6 +1,6 @@ import { StatusCodes } from "http-status-codes"; -import type { User } from "@/api/user/userModel"; +import type { IUserResponse, IUsersResponse, User } from "@/api/user/userModel"; import { UserRepository } from "@/api/user/userRepository"; import { ServiceResponse } from "@/common/models/serviceResponse"; import { logger } from "@/server"; @@ -13,7 +13,7 @@ export class UserService { } // Retrieves all users from the database - async findAll(): Promise> { + async findAll(): Promise { try { const users = await this.userRepository.findAllAsync(); if (!users || users.length === 0) { @@ -32,11 +32,11 @@ export class UserService { } // Retrieves a single user by their ID - async findById(id: number): Promise> { + async findById(id: number): Promise { try { const user = await this.userRepository.findByIdAsync(id); if (!user) { - return ServiceResponse.failure("User not found", null, StatusCodes.NOT_FOUND); + return ServiceResponse.success("User not found", null); } return ServiceResponse.success("User found", user); } catch (ex) { @@ -46,5 +46,3 @@ export class UserService { } } } - -export const userService = new UserService(); diff --git a/src/common/__tests__/errorHandler.test.ts b/src/common/__tests__/errorHandler.test.ts deleted file mode 100644 index 620b0bd..0000000 --- a/src/common/__tests__/errorHandler.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import express, { type Express } from "express"; -import { StatusCodes } from "http-status-codes"; -import request from "supertest"; - -import errorHandler from "@/common/middleware/errorHandler"; - -describe("Error Handler Middleware", () => { - let app: Express; - - beforeAll(() => { - app = express(); - - app.get("/error", () => { - throw new Error("Test error"); - }); - app.get("/next-error", (_req, _res, next) => { - const error = new Error("Error passed to next()"); - next(error); - }); - - app.use(errorHandler()); - app.use("*", (req, res) => res.status(StatusCodes.NOT_FOUND).send("Not Found")); - }); - - describe("Handling unknown routes", () => { - it("returns 404 for unknown routes", async () => { - const response = await request(app).get("/this-route-does-not-exist"); - expect(response.status).toBe(StatusCodes.NOT_FOUND); - }); - }); - - describe("Handling thrown errors", () => { - it("handles thrown errors with a 500 status code", async () => { - const response = await request(app).get("/error"); - expect(response.status).toBe(StatusCodes.INTERNAL_SERVER_ERROR); - }); - }); - - describe("Handling errors passed to next()", () => { - it("handles errors passed to next() with a 500 status code", async () => { - const response = await request(app).get("/next-error"); - expect(response.status).toBe(StatusCodes.INTERNAL_SERVER_ERROR); - }); - }); -}); diff --git a/src/common/__tests__/requestLogger.test.ts b/src/common/__tests__/requestLogger.test.ts index 625736a..8a1d235 100644 --- a/src/common/__tests__/requestLogger.test.ts +++ b/src/common/__tests__/requestLogger.test.ts @@ -2,7 +2,6 @@ import express from "express"; import { StatusCodes } from "http-status-codes"; import request from "supertest"; -import errorHandler from "@/common/middleware/errorHandler"; import requestLogger from "@/common/middleware/requestLogger"; describe("Request Logger Middleware", () => { @@ -10,12 +9,11 @@ describe("Request Logger Middleware", () => { beforeAll(() => { app.use(requestLogger); - app.get("/success", (req, res) => res.status(StatusCodes.OK).send("Success")); - app.get("/redirect", (req, res) => res.redirect("/success")); + app.get("/success", (_req, res) => res.status(StatusCodes.OK).send("Success")); + app.get("/redirect", (_req, res) => res.redirect("/success")); app.get("/error", () => { throw new Error("Test error"); }); - app.use(errorHandler()); }); describe("Successful requests", () => { diff --git a/src/common/middleware/errorHandler.ts b/src/common/middleware/errorHandler.ts index 1ecc94a..59be738 100644 --- a/src/common/middleware/errorHandler.ts +++ b/src/common/middleware/errorHandler.ts @@ -1,13 +1,52 @@ -import type { ErrorRequestHandler, RequestHandler } from "express"; +import type { NextFunction, Request, Response } from "express"; import { StatusCodes } from "http-status-codes"; +import type { ValidateError } from "tsoa"; -const unexpectedRequest: RequestHandler = (_req, res) => { - res.sendStatus(StatusCodes.NOT_FOUND); -}; +import { ServiceResponse } from "@/common/models/serviceResponse"; +import { logger } from "@/server"; -const addErrorToRequestLog: ErrorRequestHandler = (err, _req, res, next) => { - res.locals.err = err; - next(err); -}; +export function notFoundHandler(_req: Request, res: Response) { + res.status(StatusCodes.NOT_FOUND).json({ message: "Not Found" }); +} -export default () => [unexpectedRequest, addErrorToRequestLog]; +export function errorHandlers(err: unknown, req: Request, res: Response, next: NextFunction): void { + logger.error("Error occurred:", err); + + if (isValidateError(err)) { + handleValidationError(err, req, res); + } else if (err instanceof Error) { + handleGenericError(res); + } else { + handleUnknownError(res); + } + + next(); +} + +function isValidateError(error: unknown): error is ValidateError { + return ( + typeof error === "object" && + error !== null && + "name" in error && + error.name === "ValidateError" && + "fields" in error + ); +} + +function handleValidationError(err: ValidateError, req: Request, res: Response): void { + logger.warn(`Validation Error for ${req.path}:`, err.fields); + const validateErrorRes = ServiceResponse.failure("Validation Failed", err.fields); + res.status(StatusCodes.BAD_REQUEST).json(validateErrorRes); +} + +function handleGenericError(res: Response): void { + res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + message: "Internal Server Error", + }); +} + +function handleUnknownError(res: Response): void { + res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + message: "Unknown Error Occurred", + }); +} diff --git a/src/common/models/serviceResponse.ts b/src/common/models/serviceResponse.ts index 344a104..7f77c97 100644 --- a/src/common/models/serviceResponse.ts +++ b/src/common/models/serviceResponse.ts @@ -1,5 +1,4 @@ import { StatusCodes } from "http-status-codes"; -import { z } from "zod"; export class ServiceResponse { readonly success: boolean; @@ -22,11 +21,3 @@ export class ServiceResponse { return new ServiceResponse(false, message, responseObject, statusCode); } } - -export const ServiceResponseSchema = (dataSchema: T) => - z.object({ - success: z.boolean(), - message: z.string(), - responseObject: dataSchema.optional(), - statusCode: z.number(), - }); diff --git a/src/common/utils/commonValidation.ts b/src/common/utils/commonValidation.ts deleted file mode 100644 index a62a97d..0000000 --- a/src/common/utils/commonValidation.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from "zod"; - -export const commonValidations = { - id: z - .string() - .refine((data) => !Number.isNaN(Number(data)), "ID must be a numeric value") - .transform(Number) - .refine((num) => num > 0, "ID must be a positive number"), - // ... other common validations -}; diff --git a/src/common/utils/httpHandlers.ts b/src/common/utils/httpHandlers.ts deleted file mode 100644 index 1464f6b..0000000 --- a/src/common/utils/httpHandlers.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { NextFunction, Request, Response } from "express"; -import { StatusCodes } from "http-status-codes"; -import type { ZodError, ZodSchema } from "zod"; - -import { ServiceResponse } from "@/common/models/serviceResponse"; - -export const handleServiceResponse = (serviceResponse: ServiceResponse, response: Response) => { - return response.status(serviceResponse.statusCode).send(serviceResponse); -}; - -export const validateRequest = (schema: ZodSchema) => (req: Request, res: Response, next: NextFunction) => { - try { - schema.parse({ body: req.body, query: req.query, params: req.params }); - next(); - } catch (err) { - const errorMessage = `Invalid input: ${(err as ZodError).errors.map((e) => e.message).join(", ")}`; - const statusCode = StatusCodes.BAD_REQUEST; - const serviceResponse = ServiceResponse.failure(errorMessage, null, statusCode); - return handleServiceResponse(serviceResponse, res); - } -}; diff --git a/src/common/utils/tsoa-generator.ts b/src/common/utils/tsoa-generator.ts new file mode 100644 index 0000000..7c7891d --- /dev/null +++ b/src/common/utils/tsoa-generator.ts @@ -0,0 +1,60 @@ +import fs from "node:fs"; +import path from "node:path"; +import url from "node:url"; +import { logger } from "@/server"; +import { generateRoutes, generateSpec } from "tsoa"; +import type { Config, ExtendedRoutesConfig, ExtendedSpecConfig } from "tsoa"; +import ts from "typescript"; + +// Helper function to read a JSON file +const readJsonFile = (filePath: string): any => { + const fileContent = fs.readFileSync(filePath, "utf8"); + return JSON.parse(fileContent); +}; + +// Parse TypeScript configuration +const parseTsConfig = (configPath: string) => { + const tsConfigFile = ts.readConfigFile(configPath, ts.sys.readFile); + return ts.parseJsonConfigFileContent(tsConfigFile.config, ts.sys, path.dirname(configPath)); +}; + +// Main function to generate TSOA spec and routes +const buildApiSpecAndRoutes = async (): Promise => { + try { + const __filename = url.fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + // const dirname = path.dirname(new URL(import.meta.url).pathname); + const appRoot = path.resolve(__dirname, "../../../"); + + // Load configurations + const tsConfig = parseTsConfig(path.resolve(appRoot, "tsconfig.json")); + const tsoaConfig: Config = readJsonFile(path.resolve(appRoot, "tsoa.json")); + + // Combine configurations with defaults + const specOptions = { ...tsoaConfig.spec, ...tsoaConfig } as ExtendedSpecConfig; + const routeOptions = { ...tsoaConfig.routes, ...tsoaConfig } as ExtendedRoutesConfig; + + logger.info("Starting TSOA generation..."); + + logger.info("Generating OpenAPI spec..."); + await generateSpec(specOptions, tsConfig.options, tsoaConfig.ignore); + logger.info("OpenAPI spec generated successfully."); + + logger.info("Generating routes..."); + await generateRoutes(routeOptions, tsConfig.options, tsoaConfig.ignore); + logger.info("Routes generated successfully."); + + logger.info("TSOA generation complete."); + } catch (error) { + logger.error("Error during TSOA generation:", error); + process.exit(1); + } +}; + +// Check if the current script is being run directly +if (import.meta.url.includes(process.argv[1])) { + buildApiSpecAndRoutes(); +} + +export default buildApiSpecAndRoutes; diff --git a/src/server.ts b/src/server.ts index b286b5b..c341fe6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,15 +1,15 @@ import cors from "cors"; -import express, { type Express } from "express"; +import express, { type Express, type Request, type Response } from "express"; import helmet from "helmet"; import { pino } from "pino"; +import swaggerUi from "swagger-ui-express"; -import { openAPIRouter } from "@/api-docs/openAPIRouter"; -import { healthCheckRouter } from "@/api/healthCheck/healthCheckRouter"; -import { userRouter } from "@/api/user/userRouter"; -import errorHandler from "@/common/middleware/errorHandler"; -import rateLimiter from "@/common/middleware/rateLimiter"; +import { RegisterRoutes } from "@/api/generated/routes"; +import { errorHandlers, notFoundHandler } from "@/common/middleware/errorHandler"; +// import rateLimiter from "@/common/middleware/rateLimiter"; import requestLogger from "@/common/middleware/requestLogger"; import { env } from "@/common/utils/envConfig"; +import buildApiSpecAndRoutes from "@/common/utils/tsoa-generator"; const logger = pino({ name: "server start" }); const app: Express = express(); @@ -22,19 +22,24 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cors({ origin: env.CORS_ORIGIN, credentials: true })); app.use(helmet()); -app.use(rateLimiter); +// app.use(rateLimiter); // Request logging app.use(requestLogger); +if (env.isDevelopment) await buildApiSpecAndRoutes(); + // Routes -app.use("/health-check", healthCheckRouter); -app.use("/users", userRouter); +RegisterRoutes(app); // Swagger UI -app.use(openAPIRouter); +app.use("/", swaggerUi.serve, async (_req: Request, res: Response) => { + return res.send(swaggerUi.generateHTML(await import("./api/generated/swagger.json"))); +}); +// Not found handler +app.use(notFoundHandler); // Error handlers -app.use(errorHandler()); +app.use(errorHandlers); export { app, logger }; diff --git a/tsconfig.json b/tsconfig.json index 3a766ea..e7940a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,43 @@ { "compilerOptions": { + /* Basic Options */ + "incremental": true, "target": "ESNext", - "module": "CommonJS", - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - }, - "moduleResolution": "Node", + "module": "ES2022", "outDir": "dist", - "importsNotUsedAsValues": "remove", + + /* Strict Type-Checking Options */ "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + + /* Module Resolution Options */ + "moduleResolution": "Node", + "baseUrl": ".", "esModuleInterop": true, - "skipLibCheck": true, + "resolveJsonModule": true, + + /* Experimental Options */ + "experimentalDecorators": true, + + /* Advanced Options */ + "types": ["vitest/globals"], "forceConsistentCasingInFileNames": true, - "types": ["vitest/globals"] + "importsNotUsedAsValues": "remove", + "paths": { + "@/*": ["src/*"] + } }, "include": ["src/**/*"], "exclude": ["node_modules"] diff --git a/tsoa.json b/tsoa.json new file mode 100644 index 0000000..3bf7e8a --- /dev/null +++ b/tsoa.json @@ -0,0 +1,22 @@ +{ + "entryFile": "src/index.ts", + "noImplicitAdditionalProperties": "silently-remove-extras", + "controllerPathGlobs": ["src/**/*Controller.ts"], + "spec": { + "basePath": "/api/", + "outputDirectory": "src/api/generated/", + "specVersion": 3 + }, + "routes": { + "basePath": "/api/", + "routesDir": "src/api/generated/", + "middleware": "express", + "esm": true + }, + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +}