From 5a120dce76f6db3d487eb6fef677ca17245ef1a3 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 09:03:31 +0700 Subject: [PATCH 01/39] Initial commit server prototype --- Prototype/server/.gitignore | 6 + Prototype/server/.prettierignore | 18 + Prototype/server/.prettierrc | 5 + Prototype/server/nodemon.json | 6 + Prototype/server/package.json | 41 + Prototype/server/pnpm-lock.yaml | 2209 +++++++++++++++++ Prototype/server/src/app.ts | 28 + .../src/controllers/cabinetController.ts | 133 + .../server/src/controllers/fileController.ts | 24 + .../src/controllers/folderController.ts | 90 + Prototype/server/src/elasticsearch/index.ts | 7 + Prototype/server/src/interfaces/ehr-fs.ts | 32 + Prototype/server/src/interfaces/http-error.ts | 19 + .../server/src/interfaces/http-status.ts | 380 +++ Prototype/server/src/middlewares/exception.ts | 19 + Prototype/server/src/routes.ts | 460 ++++ Prototype/server/src/storage/index.ts | 11 + Prototype/server/src/swagger.json | 477 ++++ Prototype/server/src/utils/auth.ts | 39 + Prototype/server/src/utils/minio.ts | 54 + Prototype/server/static/.gitkeep | 0 Prototype/server/tsconfig.json | 18 + Prototype/server/tsoa.json | 34 + 23 files changed, 4110 insertions(+) create mode 100644 Prototype/server/.gitignore create mode 100644 Prototype/server/.prettierignore create mode 100644 Prototype/server/.prettierrc create mode 100644 Prototype/server/nodemon.json create mode 100644 Prototype/server/package.json create mode 100644 Prototype/server/pnpm-lock.yaml create mode 100644 Prototype/server/src/app.ts create mode 100644 Prototype/server/src/controllers/cabinetController.ts create mode 100644 Prototype/server/src/controllers/fileController.ts create mode 100644 Prototype/server/src/controllers/folderController.ts create mode 100644 Prototype/server/src/elasticsearch/index.ts create mode 100644 Prototype/server/src/interfaces/ehr-fs.ts create mode 100644 Prototype/server/src/interfaces/http-error.ts create mode 100644 Prototype/server/src/interfaces/http-status.ts create mode 100644 Prototype/server/src/middlewares/exception.ts create mode 100644 Prototype/server/src/routes.ts create mode 100644 Prototype/server/src/storage/index.ts create mode 100644 Prototype/server/src/swagger.json create mode 100644 Prototype/server/src/utils/auth.ts create mode 100644 Prototype/server/src/utils/minio.ts create mode 100644 Prototype/server/static/.gitkeep create mode 100644 Prototype/server/tsconfig.json create mode 100644 Prototype/server/tsoa.json diff --git a/Prototype/server/.gitignore b/Prototype/server/.gitignore new file mode 100644 index 0000000..b740314 --- /dev/null +++ b/Prototype/server/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +node_modules +/dist +.env +.env.* +!.env.example diff --git a/Prototype/server/.prettierignore b/Prototype/server/.prettierignore new file mode 100644 index 0000000..db4f49d --- /dev/null +++ b/Prototype/server/.prettierignore @@ -0,0 +1,18 @@ +.DS_Store +node_modules +.env +.env.* +!.env.example + +/dist +/static +/src/routes.ts +/src/swagger.json + +# Any log +*.log + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/Prototype/server/.prettierrc b/Prototype/server/.prettierrc new file mode 100644 index 0000000..72bb8a7 --- /dev/null +++ b/Prototype/server/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "printWidth": 100, + "trailingComma": "all" +} diff --git a/Prototype/server/nodemon.json b/Prototype/server/nodemon.json new file mode 100644 index 0000000..8078e53 --- /dev/null +++ b/Prototype/server/nodemon.json @@ -0,0 +1,6 @@ +{ + "exec": "ts-node src/app.ts", + "ext": "ts", + "watch": ["src"], + "ignore": ["src/routes.ts"] +} diff --git a/Prototype/server/package.json b/Prototype/server/package.json new file mode 100644 index 0000000..d91336c --- /dev/null +++ b/Prototype/server/package.json @@ -0,0 +1,41 @@ +{ + "name": "test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "format": "prettier --write .", + "dev": "concurrently \"nodemon\" \"nodemon -x tsoa spec-and-routes\"", + "build": "tsoa spec-and-route && tsc", + "preview": "node ./dist/app.js", + "serve": "node ./dist/app.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@elastic/elasticsearch": "^8.10.0", + "@tsoa/runtime": "^5.0.0", + "@types/cors": "^2.8.16", + "@types/jsonwebtoken": "^9.0.5", + "@types/multer": "^1.4.10", + "concurrently": "^8.2.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "fast-jwt": "^3.3.1", + "minio": "^7.1.3", + "multer": "1.4.5-lts.1", + "prettier": "^3.1.0", + "swagger-ui-express": "^5.0.0", + "tsoa": "^5.1.1" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.9.0", + "@types/swagger-ui-express": "^4.1.6", + "nodemon": "^3.0.1", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + } +} diff --git a/Prototype/server/pnpm-lock.yaml b/Prototype/server/pnpm-lock.yaml new file mode 100644 index 0000000..4bbe319 --- /dev/null +++ b/Prototype/server/pnpm-lock.yaml @@ -0,0 +1,2209 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@elastic/elasticsearch': + specifier: ^8.10.0 + version: 8.10.0 + '@tsoa/runtime': + specifier: ^5.0.0 + version: 5.0.0 + '@types/cors': + specifier: ^2.8.16 + version: 2.8.16 + '@types/jsonwebtoken': + specifier: ^9.0.5 + version: 9.0.5 + '@types/multer': + specifier: ^1.4.10 + version: 1.4.10 + concurrently: + specifier: ^8.2.2 + version: 8.2.2 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dotenv: + specifier: ^16.3.1 + version: 16.3.1 + express: + specifier: ^4.18.2 + version: 4.18.2 + fast-jwt: + specifier: ^3.3.1 + version: 3.3.1 + minio: + specifier: ^7.1.3 + version: 7.1.3 + multer: + specifier: 1.4.5-lts.1 + version: 1.4.5-lts.1 + prettier: + specifier: ^3.1.0 + version: 3.1.0 + swagger-ui-express: + specifier: ^5.0.0 + version: 5.0.0(express@4.18.2) + tsoa: + specifier: ^5.1.1 + version: 5.1.1 + +devDependencies: + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/node': + specifier: ^20.9.0 + version: 20.9.0 + '@types/swagger-ui-express': + specifier: ^4.1.6 + version: 4.1.6 + nodemon: + specifier: ^3.0.1 + version: 3.0.1 + ts-node: + specifier: ^10.9.1 + version: 10.9.1(@types/node@20.9.0)(typescript@5.2.2) + typescript: + specifier: ^5.2.2 + version: 5.2.2 + +packages: + + /@babel/runtime@7.23.2: + resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: false + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@elastic/elasticsearch@8.10.0: + resolution: {integrity: sha512-RIEyqz0D18bz/dK+wJltaak+7wKaxDELxuiwOJhuMrvbrBsYDFnEoTdP/TZ0YszHBgnRPGqBDBgH/FHNgHObiQ==} + engines: {node: '>=14'} + dependencies: + '@elastic/transport': 8.3.4 + tslib: 2.6.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@elastic/transport@8.3.4: + resolution: {integrity: sha512-+0o8o74sbzu3BO7oOZiP9ycjzzdOt4QwmMEjFc1zfO7M0Fh7QX1xrpKqZbSd8vBwihXNlSq/EnMPfgD2uFEmFg==} + engines: {node: '>=14'} + dependencies: + debug: 4.3.4 + hpagent: 1.2.0 + ms: 2.1.3 + secure-json-parse: 2.7.0 + tslib: 2.6.2 + undici: 5.27.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@fastify/busboy@2.1.0: + resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} + engines: {node: '>=14'} + dev: false + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@lukeed/ms@2.0.1: + resolution: {integrity: sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==} + engines: {node: '>=8'} + dev: false + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@tsoa/cli@5.1.1: + resolution: {integrity: sha512-krvp6Qr2yPUfj6bJRs0vwQhLANeINzyusNnzgSoerDfBBBnjZ+VhvR4rWguAcLc1kgP/kFAJz5kIp4iqLFmILQ==} + engines: {node: '>=12.0.0', yarn: '>=1.9.4'} + hasBin: true + dependencies: + '@tsoa/runtime': 5.0.0 + deepmerge: 4.3.1 + fs-extra: 10.1.0 + glob: 8.1.0 + handlebars: 4.7.8 + merge: 2.1.1 + minimatch: 5.1.6 + typescript: 4.9.5 + validator: 13.11.0 + yamljs: 0.3.0 + yargs: 17.7.2 + dev: false + + /@tsoa/runtime@5.0.0: + resolution: {integrity: sha512-DY0x7ZhNRF9FcwCZXQQbQhVj3bfZe0LScNyqp0c8PhDTj0gRMjY4ESVpihopRzhQtamReJoDRg3FhEu4BlSVtA==} + engines: {node: '>=12.0.0', yarn: '>=1.9.4'} + dependencies: + '@types/multer': 1.4.10 + promise.any: 2.0.6 + reflect-metadata: 0.1.13 + validator: 13.11.0 + dev: false + + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.9.0 + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.9.0 + + /@types/cors@2.8.16: + resolution: {integrity: sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==} + dependencies: + '@types/node': 20.9.0 + dev: false + + /@types/express-serve-static-core@4.17.41: + resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} + dependencies: + '@types/node': 20.9.0 + '@types/qs': 6.9.10 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.17.41 + '@types/qs': 6.9.10 + '@types/serve-static': 1.15.5 + + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + /@types/jsonwebtoken@9.0.5: + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + dependencies: + '@types/node': 20.9.0 + dev: false + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + /@types/mime@3.0.4: + resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} + + /@types/multer@1.4.10: + resolution: {integrity: sha512-6l9mYMhUe8wbnz/67YIjc7ZJyQNZoKq7fRXVf7nMdgWgalD0KyzJ2ywI7hoATUSXSbTu9q2HBiEwzy0tNN1v2w==} + dependencies: + '@types/express': 4.17.21 + dev: false + + /@types/node@20.9.0: + resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==} + dependencies: + undici-types: 5.26.5 + + /@types/qs@6.9.10: + resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==} + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.9.0 + + /@types/serve-static@1.15.5: + resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/mime': 3.0.4 + '@types/node': 20.9.0 + + /@types/swagger-ui-express@4.1.6: + resolution: {integrity: sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==} + dependencies: + '@types/express': 4.17.21 + '@types/serve-static': 1.15.5 + dev: true + + /@zxing/text-encoding@0.9.0: + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + requiresBuild: true + dev: false + optional: true + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-walk@8.3.0: + resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: false + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + dev: false + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: false + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.5 + is-array-buffer: 3.0.2 + dev: false + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /array.prototype.map@1.0.6: + resolution: {integrity: sha512-nK1psgF2cXqP3wSyCSq0Hc7zwNq3sfljQqaG27r/7a7ooNUnn5nGq6yYWyks9jMO5EoFQ0ax80hSg6oXSRNXaw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-array-method-boxes-properly: 1.0.0 + is-string: 1.0.7 + dev: false + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: false + + /asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + dev: false + + /async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + dev: false + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + dependencies: + readable-stream: 3.6.2 + dev: false + + /bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: false + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: false + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + dev: false + + /buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false + + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: false + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: false + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: false + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + dev: false + + /concurrently@8.2.2: + resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} + engines: {node: ^14.13.0 || >=16.0.0} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.21 + rxjs: 7.8.1 + shell-quote: 1.8.1 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: false + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: false + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.23.2 + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /debug@3.2.7(supports-color@5.5.0): + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 5.5.0 + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + /decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + dev: false + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: false + + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 + object-keys: 1.1.1 + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: false + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /es-abstract@1.22.3: + resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + es-set-tostringtag: 2.0.2 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.2 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + internal-slot: 1.0.6 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.13 + dev: false + + /es-aggregate-error@1.0.11: + resolution: {integrity: sha512-DCiZiNlMlbvofET/cE55My387NiLvuGToBEZDdK9U2G3svDCjL8WOgO5Il6lO83nQ8qmag/R9nArdpaFQ/m3lA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + define-properties: 1.2.1 + es-abstract: 1.22.3 + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + globalthis: 1.0.3 + has-property-descriptors: 1.0.1 + set-function-name: 2.0.1 + dev: false + + /es-array-method-boxes-properly@1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + dev: false + + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: false + + /es-set-tostringtag@2.0.2: + resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + has-tostringtag: 1.0.0 + hasown: 2.0.0 + dev: false + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: false + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /fast-jwt@3.3.1: + resolution: {integrity: sha512-1YuuIJeh1hEvfcYDe89P2oGACWI5hd2GadRDKHalSxkc1Z0z8I6yzuVK6SF15sW09QZngTV6d7g4+TFL9bvs5A==} + engines: {node: '>=16 <22'} + dependencies: + '@lukeed/ms': 2.0.1 + asn1.js: 5.4.1 + ecdsa-sig-formatter: 1.0.11 + mnemonist: 0.39.5 + dev: false + + /fast-xml-parser@4.3.2: + resolution: {integrity: sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + dev: false + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: false + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + functions-have-names: 1.2.3 + dev: false + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: false + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: false + + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: false + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: false + + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: false + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: false + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: false + + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.17.4 + dev: false + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: false + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: false + + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + dev: false + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: false + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /internal-slot@1.0.6: + resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + hasown: 2.0.0 + side-channel: 1.0.4 + dev: false + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /ipaddr.js@2.1.0: + resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} + engines: {node: '>= 10'} + dev: false + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: false + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: false + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: false + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: false + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: false + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-map@2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + dev: false + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: false + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: false + + /is-set@2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + dev: false + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.5 + dev: false + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.13 + dev: false + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.5 + dev: false + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: false + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: false + + /iterate-iterator@1.0.2: + resolution: {integrity: sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==} + dev: false + + /iterate-value@1.0.2: + resolution: {integrity: sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==} + dependencies: + es-get-iterator: 1.1.3 + iterate-iterator: 1.0.2 + dev: false + + /json-stream@1.0.0: + resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==} + dev: false + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /merge@2.1.1: + resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} + dev: false + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + + /minio@7.1.3: + resolution: {integrity: sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==} + engines: {node: ^16 || ^18 || >=20} + dependencies: + async: 3.2.5 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 0.2.13 + fast-xml-parser: 4.3.2 + ipaddr.js: 2.1.0 + json-stream: 1.0.0 + lodash: 4.17.21 + mime-types: 2.1.35 + query-string: 7.1.3 + through2: 4.0.2 + web-encoding: 1.1.5 + xml: 1.0.1 + xml2js: 0.5.0 + dev: false + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: false + + /mnemonist@0.39.5: + resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==} + dependencies: + obliterator: 2.0.4 + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /multer@1.4.5-lts.1: + resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==} + engines: {node: '>= 6.0.0'} + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + dev: false + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: false + + /nodemon@3.0.1: + resolution: {integrity: sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chokidar: 3.5.3 + debug: 3.2.7(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.5.4 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.0 + undefsafe: 2.0.5 + dev: true + + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: false + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + + /obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: false + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /prettier@3.1.0: + resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} + engines: {node: '>=14'} + hasBin: true + dev: false + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: false + + /promise.any@2.0.6: + resolution: {integrity: sha512-Ew/MrPtTjiHnnki0AA2hS2o65JaZ5n+5pp08JSyWWUdeOGF4F41P+Dn+rdqnaOV/FTxhR6eBDX412luwn3th9g==} + engines: {node: '>= 0.4'} + dependencies: + array.prototype.map: 1.0.6 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-aggregate-error: 1.0.11 + get-intrinsic: 1.2.2 + iterate-value: 1.0.2 + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: true + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: false + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /reflect-metadata@0.1.13: + resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + dev: false + + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: false + + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: false + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: false + + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.6.2 + dev: false + + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: false + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-regex: 1.1.4 + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + dev: false + + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: false + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.1 + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + dev: false + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 + dev: false + + /simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: false + + /spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + dev: false + + /split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + dev: false + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: false + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.6 + dev: false + + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + + /strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + dev: false + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: false + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: false + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: false + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: false + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: false + + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: false + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: false + + /swagger-ui-dist@5.9.4: + resolution: {integrity: sha512-Ppghvj6Q8XxH5xiSrUjEeCUitrasGtz7v9FCUIBR/4t89fACQ4FnUT9D0yfodUYhB+PrCmYmxwe/2jTDLslHDw==} + dev: false + + /swagger-ui-express@5.0.0(express@4.18.2): + resolution: {integrity: sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + dependencies: + express: 4.18.2 + swagger-ui-dist: 5.9.4 + dev: false + + /through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + dependencies: + readable-stream: 3.6.2 + dev: false + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /touch@3.1.0: + resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} + hasBin: true + dependencies: + nopt: 1.0.10 + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: false + + /ts-node@10.9.1(@types/node@20.9.0)(typescript@5.2.2): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.9.0 + acorn: 8.11.2 + acorn-walk: 8.3.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.2.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + + /tsoa@5.1.1: + resolution: {integrity: sha512-U6+5CyD3+u9Dtza0fBnv4+lgmbZEskYljzRpKf3edGCAGtMKD2rfjtDw9jUdTfWb1FEDvsnR3pRvsSGBXaOdsA==} + engines: {node: '>=12.0.0', yarn: '>=1.9.4'} + hasBin: true + dependencies: + '@tsoa/cli': 5.1.1 + '@tsoa/runtime': 5.0.0 + dev: false + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: false + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: false + + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: false + + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false + + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: false + optional: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.5 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: false + + /undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + /undici@5.27.2: + resolution: {integrity: sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.1.0 + dev: false + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: false + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.12 + which-typed-array: 1.1.13 + dev: false + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /validator@13.11.0: + resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} + engines: {node: '>= 0.10'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + dev: false + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: false + + /which-typed-array@1.1.13: + resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: false + + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: false + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: false + + /xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.3.0 + xmlbuilder: 11.0.1 + dev: false + + /xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + dev: false + + /xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: false + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: false + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yamljs@0.3.0: + resolution: {integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==} + hasBin: true + dependencies: + argparse: 1.0.10 + glob: 7.2.3 + dev: false + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: false + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + 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.8 + yargs-parser: 21.1.1 + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true diff --git a/Prototype/server/src/app.ts b/Prototype/server/src/app.ts new file mode 100644 index 0000000..ac593ed --- /dev/null +++ b/Prototype/server/src/app.ts @@ -0,0 +1,28 @@ +import "dotenv/config"; +import express from "express"; +import swaggerUi from "swagger-ui-express"; +import cors from "cors"; + +import { RegisterRoutes } from "./routes"; +import errorHandler from "./middlewares/exception"; + +import swaggerSpecs from "./swagger.json"; + +const PORT = +(process.env.PORT || 80); + +const app = express(); +const router = express.Router(); + +if (process.env.NODE_ENV !== "production") app.use(cors()); + +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(express.static("static")); +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { explorer: false })); + +RegisterRoutes(router); + +app.use(swaggerSpecs.basePath, router); +app.use(errorHandler); + +app.listen(PORT, () => console.log(`Application is running on http://localhost:${PORT}`)); diff --git a/Prototype/server/src/controllers/cabinetController.ts b/Prototype/server/src/controllers/cabinetController.ts new file mode 100644 index 0000000..43ffa6a --- /dev/null +++ b/Prototype/server/src/controllers/cabinetController.ts @@ -0,0 +1,133 @@ +import { + Body, + Controller, + Delete, + Example, + Get, + Path, + Post, + Put, + Route, + Security, + SuccessResponse, + Response, + Tags, +} from "tsoa"; +import * as Minio from "minio"; +import minioClient from "../storage"; + +import { EhrFolder } from "../interfaces/ehr-fs"; +import HttpStatusCode from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { listFolder, pathExist } from "../utils/minio"; + +@Route("cabinet") +export class CabinetController extends Controller { + @Get("/") + @Tags("Cabinet") + @SuccessResponse(HttpStatusCode.OK) + public listCabinet(): Promise { + return listFolder(); + } + + @Post("/") + @Tags("Cabinet") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.CREATED) + public async createCabinet(@Body() body: { name: string }) { + const uploaded = await minioClient + .putObject("ehr", `${body.name}/.keep`, "", 0, { + createdAt: new Date().toISOString(), + createdBy: "SomeUser", + }) + .catch((e) => console.error(e)); + + if (!uploaded) throw new Error("Object storage error occured."); + + return this.setStatus(HttpStatusCode.CREATED); + } + + @Put("/{cabinetName}") + @Tags("Cabinet") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "Success") + public async editCabinet( + @Path() cabinetName: string, + @Body() body: { name: string }, + ): Promise { + return new Promise((resolve, reject) => { + const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/`, true); + + stream.on("data", (v) => { + if (!(v && v.name)) return; + + const destination = `${body.name}/${v.name.slice(cabinetName.length + 1)}`; + const source = `/ehr/${v.name}`; + const cond = new Minio.CopyConditions(); + + minioClient.copyObject("ehr", destination, source, cond, (e) => { + if (e) { + return reject(new Error("Failed to move.")); + } + return minioClient.removeObject("ehr", v.name); + }); + }); + + stream.on("end", () => { + this.setStatus(HttpStatusCode.NO_CONTENT); + resolve(); + }); + stream.on("error", () => reject(new Error("Object storage error occured."))); + }); + } + + @Delete("/{cabinetName}") + @Tags("Cabinet") + @SuccessResponse(HttpStatusCode.NO_CONTENT) + public async deleteCabinet(@Path() cabinetName: string) { + return new Promise((resolve, reject) => { + const objects: string[] = []; + const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/`, true); + + stream.on("data", (v) => { + if (!(v && v.name)) return; + + objects.push(v.name); + }); + + stream.on("close", () => minioClient.removeObjects("ehr", objects)); + stream.on("error", () => reject(new Error("Object storage error occured."))); + + resolve(true); + }); + } + + @Get("/{cabinetName}/drawer") + @Tags("Drawer") + @SuccessResponse(HttpStatusCode.OK) + public listDrawer(@Path() cabinetName: string) { + return listFolder(`${cabinetName}/`); + } + + @Post("/{cabinetName}/drawer") + @Tags("Drawer") + @SuccessResponse(HttpStatusCode.NO_CONTENT) + public async createDrawer(@Path() cabinetName: string, @Body() body: { name: string }) { + if (!(await pathExist(`${cabinetName}/`))) { + throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet cannot be found."); + } + + const uploaded = await minioClient + .putObject("ehr", `${cabinetName}/${body.name}/.keep`, "", 0, { + createdAt: new Date().toISOString(), + createdBy: "SomeUser", + }) + .catch((e) => console.error(e)); + + if (!uploaded) { + throw new Error("Object storage error occured."); + } + + this.setStatus(HttpStatusCode.CREATED); + return; + } +} diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts new file mode 100644 index 0000000..c43947a --- /dev/null +++ b/Prototype/server/src/controllers/fileController.ts @@ -0,0 +1,24 @@ +import { Controller, FormField, Post, Route, UploadedFile } from "tsoa"; +import minioClient from "../storage"; +import esClient from "../elasticsearch"; + +@Route("/file") +export class FileController extends Controller { + @Post("/") + public async uploadFile(@UploadedFile() file: Express.Multer.File, @FormField() desc: string) { + const filename = Buffer.from(file.originalname, "latin1").toString("utf-8"); + + console.log( + esClient.search({ + query: { + match_all: {}, + }, + }), + ); + + minioClient.putObject("ehr", `test_upload_file/${filename}`, file.buffer, file.size, { + "Content-Type": file.mimetype, + }); + return; + } +} diff --git a/Prototype/server/src/controllers/folderController.ts b/Prototype/server/src/controllers/folderController.ts new file mode 100644 index 0000000..3cd0380 --- /dev/null +++ b/Prototype/server/src/controllers/folderController.ts @@ -0,0 +1,90 @@ +import { Body, Controller, Get, Post, Put, Query, Route, SuccessResponse, Tags } from "tsoa"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { listFolder, pathExist } from "../utils/minio"; +import { EhrFolder } from "../interfaces/ehr-fs"; +import minioClient from "../storage"; + +@Route("/folder") +export class FolderController extends Controller { + @Get("/") + @Tags("Folder") + @SuccessResponse(HttpStatusCode.OK, "List of folder under drawer or under subfolder") + public async listFolder( + @Query() cabinet: string, + @Query() drawer: string, + @Query() path?: string, + ): Promise { + const fullpath = + [cabinet, drawer, path?.replace(/^\/|\/$/g, "")].filter((v) => !!v).join("/") + "/"; + + if (!(await pathExist(fullpath))) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); + } + + return listFolder(fullpath); + } + + @Post("/") + @Tags("Folder") + @SuccessResponse(HttpStatusCode.CREATED, "Folder created.") + public async createFolder( + @Body() body: { name: string }, + @Query() cabinet: string, + @Query() drawer: string, + @Query() path?: string, + ) { + const fullpath = + [cabinet, drawer, path?.replace(/^\/|\/$/g, "")].filter((v) => !!v).join("/") + "/"; + + if (!(await pathExist(fullpath))) { + throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Provided path does not exist."); + } + + const uploaded = await minioClient + .putObject("ehr", `${fullpath}${body.name}/.keep`, "", 0, { + createdAt: new Date().toISOString(), + createdBy: "SomeUser", + }) + .catch((e) => console.error(e)); + + if (!uploaded) { + throw new Error("Object storage error occured."); + } + + this.setStatus(HttpStatusCode.CREATED); + return; + } + + @Put("/") + @Tags("Folder") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "Folder name changed.") + public async editFolder( + @Body() body: { name: string }, + @Query() cabinet: string, + @Query() drawer: string, + @Query() path: string, + ) { + const fullpath = + [cabinet, drawer, path.replace(/^\/|\/$/g, "")].filter((v) => !!v).join("/") + "/"; + + if (!(await pathExist(fullpath))) { + throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Provided path does not exist."); + } + + // TODO: Recursive get object and move + // const uploaded = await minioClient + // .putObject("ehr", `${fullpath}${body.name}/.keep`, "", 0, { + // createdAt: new Date().toISOString(), + // createdBy: "SomeUser", + // }) + // .catch((e) => console.error(e)); + // + // if (!uploaded) { + // throw new Error("Object storage error occured."); + // } + + this.setStatus(HttpStatusCode.CREATED); + return; + } +} diff --git a/Prototype/server/src/elasticsearch/index.ts b/Prototype/server/src/elasticsearch/index.ts new file mode 100644 index 0000000..b1c1552 --- /dev/null +++ b/Prototype/server/src/elasticsearch/index.ts @@ -0,0 +1,7 @@ +import { Client } from "@elastic/elasticsearch"; + +const esClient = new Client({ + node: "http://localhost:9200", +}); + +export default esClient; diff --git a/Prototype/server/src/interfaces/ehr-fs.ts b/Prototype/server/src/interfaces/ehr-fs.ts new file mode 100644 index 0000000..5916436 --- /dev/null +++ b/Prototype/server/src/interfaces/ehr-fs.ts @@ -0,0 +1,32 @@ +export interface EhrFolder { + /** + * @prop Full path to this folder. It is used as key as there are no files or directories at the same location. + */ + pathname: string; + /** + * @prop Directory / Folder name. + */ + name: string; + + createdAt: string | Date; + createdBy: string | Date; +} + +export interface EhrFile { + /** + * @prop Full path to this folder. It is used as key as there are no files or directories at the same location. + */ + pathname: string; + + fileName: string; + fileSize: string; + fileType: string; + + category: string[]; + keyword: string[]; + + updatedAt: string | Date; + updatedBy: string; + createdAt: string | Date; + createdBy: string; +} diff --git a/Prototype/server/src/interfaces/http-error.ts b/Prototype/server/src/interfaces/http-error.ts new file mode 100644 index 0000000..4303a62 --- /dev/null +++ b/Prototype/server/src/interfaces/http-error.ts @@ -0,0 +1,19 @@ +import HttpStatusCode from "./http-status"; + +class HttpError extends Error { + /** + * HTTP Status Code + */ + status: HttpStatusCode; + message: string; + + constructor(status: HttpStatusCode, message: string) { + super(message); + + this.name = "HttpError"; + this.status = status; + this.message = message; + } +} + +export default HttpError; diff --git a/Prototype/server/src/interfaces/http-status.ts b/Prototype/server/src/interfaces/http-status.ts new file mode 100644 index 0000000..92fbab2 --- /dev/null +++ b/Prototype/server/src/interfaces/http-status.ts @@ -0,0 +1,380 @@ +/** + * Hypertext Transfer Protocol (HTTP) response status codes. + * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} + */ +enum HttpStatusCode { + /** + * The server has received the request headers and the client should proceed to send the request body + * (in the case of a request for which a body needs to be sent; for example, a POST request). + * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient. + * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request + * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued. + */ + CONTINUE = 100, + + /** + * The requester has asked the server to switch protocols and the server has agreed to do so. + */ + SWITCHING_PROTOCOLS = 101, + + /** + * A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request. + * This code indicates that the server has received and is processing the request, but no response is available yet. + * This prevents the client from timing out and assuming the request was lost. + */ + PROCESSING = 102, + + /** + * Standard response for successful HTTP requests. + * The actual response will depend on the request method used. + * In a GET request, the response will contain an entity corresponding to the requested resource. + * In a POST request, the response will contain an entity describing or containing the result of the action. + */ + OK = 200, + + /** + * The request has been fulfilled, resulting in the creation of a new resource. + */ + CREATED = 201, + + /** + * The request has been accepted for processing, but the processing has not been completed. + * The request might or might not be eventually acted upon, and may be disallowed when processing occurs. + */ + ACCEPTED = 202, + + /** + * SINCE HTTP/1.1 + * The server is a transforming proxy that received a 200 OK from its origin, + * but is returning a modified version of the origin's response. + */ + NON_AUTHORITATIVE_INFORMATION = 203, + + /** + * The server successfully processed the request and is not returning any content. + */ + NO_CONTENT = 204, + + /** + * The server successfully processed the request, but is not returning any content. + * Unlike a 204 response, this response requires that the requester reset the document view. + */ + RESET_CONTENT = 205, + + /** + * The server is delivering only part of the resource (byte serving) due to a range header sent by the client. + * The range header is used by HTTP clients to enable resuming of interrupted downloads, + * or split a download into multiple simultaneous streams. + */ + PARTIAL_CONTENT = 206, + + /** + * The message body that follows is an XML message and can contain a number of separate response codes, + * depending on how many sub-requests were made. + */ + MULTI_STATUS = 207, + + /** + * The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response, + * and are not being included again. + */ + ALREADY_REPORTED = 208, + + /** + * The server has fulfilled a request for the resource, + * and the response is a representation of the result of one or more instance-manipulations applied to the current instance. + */ + IM_USED = 226, + + /** + * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation). + * For example, this code could be used to present multiple video format options, + * to list files with different filename extensions, or to suggest word-sense disambiguation. + */ + MULTIPLE_CHOICES = 300, + + /** + * This and all future requests should be directed to the given URI. + */ + MOVED_PERMANENTLY = 301, + + /** + * This is an example of industry practice contradicting the standard. + * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect + * (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 + * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 + * to distinguish between the two behaviours. However, some Web applications and frameworks + * use the 302 status code as if it were the 303. + */ + FOUND = 302, + + /** + * SINCE HTTP/1.1 + * The response to the request can be found under another URI using a GET method. + * When received in response to a POST (or PUT/DELETE), the client should presume that + * the server has received the data and should issue a redirect with a separate GET message. + */ + SEE_OTHER = 303, + + /** + * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. + * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy. + */ + NOT_MODIFIED = 304, + + /** + * SINCE HTTP/1.1 + * The requested resource is available only through a proxy, the address for which is provided in the response. + * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons. + */ + USE_PROXY = 305, + + /** + * No longer used. Originally meant "Subsequent requests should use the specified proxy." + */ + SWITCH_PROXY = 306, + + /** + * SINCE HTTP/1.1 + * In this case, the request should be repeated with another URI; however, future requests should still use the original URI. + * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. + * For example, a POST request should be repeated using another POST request. + */ + TEMPORARY_REDIRECT = 307, + + /** + * The request and all future requests should be repeated using another URI. + * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. + * So, for example, submitting a form to a permanently redirected resource may continue smoothly. + */ + PERMANENT_REDIRECT = 308, + + /** + * The server cannot or will not process the request due to an apparent client error + * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing). + */ + BAD_REQUEST = 400, + + /** + * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet + * been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the + * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means + * "unauthenticated",i.e. the user does not have the necessary credentials. + */ + UNAUTHORIZED = 401, + + /** + * Reserved for future use. The original intention was that this code might be used as part of some form of digital + * cash or micro payment scheme, but that has not happened, and this code is not usually used. + * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests. + */ + PAYMENT_REQUIRED = 402, + + /** + * The request was valid, but the server is refusing action. + * The user might not have the necessary permissions for a resource. + */ + FORBIDDEN = 403, + + /** + * The requested resource could not be found but may be available in the future. + * Subsequent requests by the client are permissible. + */ + NOT_FOUND = 404, + + /** + * A request method is not supported for the requested resource; + * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. + */ + METHOD_NOT_ALLOWED = 405, + + /** + * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. + */ + NOT_ACCEPTABLE = 406, + + /** + * The client must first authenticate itself with the proxy. + */ + PROXY_AUTHENTICATION_REQUIRED = 407, + + /** + * The server timed out waiting for the request. + * According to HTTP specifications: + * "The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time." + */ + REQUEST_TIMEOUT = 408, + + /** + * Indicates that the request could not be processed because of conflict in the request, + * such as an edit conflict between multiple simultaneous updates. + */ + CONFLICT = 409, + + /** + * Indicates that the resource requested is no longer available and will not be available again. + * This should be used when a resource has been intentionally removed and the resource should be purged. + * Upon receiving a 410 status code, the client should not request the resource in the future. + * Clients such as search engines should remove the resource from their indices. + * Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead. + */ + GONE = 410, + + /** + * The request did not specify the length of its content, which is required by the requested resource. + */ + LENGTH_REQUIRED = 411, + + /** + * The server does not meet one of the preconditions that the requester put on the request. + */ + PRECONDITION_FAILED = 412, + + /** + * The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large". + */ + PAYLOAD_TOO_LARGE = 413, + + /** + * The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request, + * in which case it should be converted to a POST request. + * Called "Request-URI Too Long" previously. + */ + URI_TOO_LONG = 414, + + /** + * The request entity has a media type which the server or resource does not support. + * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format. + */ + UNSUPPORTED_MEDIA_TYPE = 415, + + /** + * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. + * For example, if the client asked for a part of the file that lies beyond the end of the file. + * Called "Requested Range Not Satisfiable" previously. + */ + RANGE_NOT_SATISFIABLE = 416, + + /** + * The server cannot meet the requirements of the Expect request-header field. + */ + EXPECTATION_FAILED = 417, + + /** + * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, + * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by + * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com. + */ + I_AM_A_TEAPOT = 418, + + /** + * The request was directed at a server that is not able to produce a response (for example because a connection reuse). + */ + MISDIRECTED_REQUEST = 421, + + /** + * The request was well-formed but was unable to be followed due to semantic errors. + */ + UNPROCESSABLE_ENTITY = 422, + + /** + * The resource that is being accessed is locked. + */ + LOCKED = 423, + + /** + * The request failed due to failure of a previous request (e.g., a PROPPATCH). + */ + FAILED_DEPENDENCY = 424, + + /** + * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. + */ + UPGRADE_REQUIRED = 426, + + /** + * The origin server requires the request to be conditional. + * Intended to prevent "the 'lost update' problem, where a client + * GETs a resource's state, modifies it, and PUTs it back to the server, + * when meanwhile a third party has modified the state on the server, leading to a conflict." + */ + PRECONDITION_REQUIRED = 428, + + /** + * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. + */ + TOO_MANY_REQUESTS = 429, + + /** + * The server is unwilling to process the request because either an individual header field, + * or all the header fields collectively, are too large. + */ + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + + /** + * A server operator has received a legal demand to deny access to a resource or to a set of resources + * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451. + */ + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + + /** + * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. + */ + INTERNAL_SERVER_ERROR = 500, + + /** + * The server either does not recognize the request method, or it lacks the ability to fulfill the request. + * Usually this implies future availability (e.g., a new feature of a web-service API). + */ + NOT_IMPLEMENTED = 501, + + /** + * The server was acting as a gateway or proxy and received an invalid response from the upstream server. + */ + BAD_GATEWAY = 502, + + /** + * The server is currently unavailable (because it is overloaded or down for maintenance). + * Generally, this is a temporary state. + */ + SERVICE_UNAVAILABLE = 503, + + /** + * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. + */ + GATEWAY_TIMEOUT = 504, + + /** + * The server does not support the HTTP protocol version used in the request + */ + HTTP_VERSION_NOT_SUPPORTED = 505, + + /** + * Transparent content negotiation for the request results in a circular reference. + */ + VARIANT_ALSO_NEGOTIATES = 506, + + /** + * The server is unable to store the representation needed to complete the request. + */ + INSUFFICIENT_STORAGE = 507, + + /** + * The server detected an infinite loop while processing the request. + */ + LOOP_DETECTED = 508, + + /** + * Further extensions to the request are required for the server to fulfill it. + */ + NOT_EXTENDED = 510, + + /** + * The client needs to authenticate to gain network access. + * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used + * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). + */ + NETWORK_AUTHENTICATION_REQUIRED = 511, +} + +export default HttpStatusCode; diff --git a/Prototype/server/src/middlewares/exception.ts b/Prototype/server/src/middlewares/exception.ts new file mode 100644 index 0000000..dcf36c3 --- /dev/null +++ b/Prototype/server/src/middlewares/exception.ts @@ -0,0 +1,19 @@ +import { NextFunction, Request, Response } from "express"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; + +function errorHandler(error: Error, _req: Request, res: Response, _next: NextFunction) { + if (error instanceof HttpError) { + return res.status(error.status).json({ + status: error.status, + message: error.message, + }); + } + + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR).json({ + status: HttpStatusCode.INTERNAL_SERVER_ERROR, + message: error.message, + }); +} + +export default errorHandler; diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts new file mode 100644 index 0000000..e647224 --- /dev/null +++ b/Prototype/server/src/routes.ts @@ -0,0 +1,460 @@ +/* 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 { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse, fetchMiddlewares } from '@tsoa/runtime'; +// 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 { CabinetController } from './controllers/cabinetController'; +// 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 { FileController } from './controllers/fileController'; +// 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 { FolderController } from './controllers/folderController'; +import { expressAuthentication } from './utils/auth'; +// @ts-ignore - no great way to install types from subpackage +const promiseAny = require('promise.any'); +import type { RequestHandler, Router } from 'express'; +const multer = require('multer'); +const upload = multer(); + +// 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 = { + "EhrFolder": { + "dataType": "refObject", + "properties": { + "pathname": {"dataType":"string","required":true}, + "name": {"dataType":"string","required":true}, + "createdAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true}, + "createdBy": {"dataType":"union","subSchemas":[{"dataType":"string"},{"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 +}; +const validationService = new ValidationService(models); + +// 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('/cabinet', + ...(fetchMiddlewares(CabinetController)), + ...(fetchMiddlewares(CabinetController.prototype.listCabinet)), + + function CabinetController_listCabinet(request: any, response: any, next: any) { + const args = { + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new CabinetController(); + + + const promise = controller.listCabinet.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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.post('/cabinet', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(fetchMiddlewares(CabinetController)), + ...(fetchMiddlewares(CabinetController.prototype.createCabinet)), + + function CabinetController_createCabinet(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":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 + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new CabinetController(); + + + const promise = controller.createCabinet.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 201, next); + } 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.put('/cabinet/:cabinetName', + ...(fetchMiddlewares(CabinetController)), + ...(fetchMiddlewares(CabinetController.prototype.editCabinet)), + + function CabinetController_editCabinet(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":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 + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new CabinetController(); + + + const promise = controller.editCabinet.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 204, next); + } 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.delete('/cabinet/:cabinetName', + ...(fetchMiddlewares(CabinetController)), + ...(fetchMiddlewares(CabinetController.prototype.deleteCabinet)), + + function CabinetController_deleteCabinet(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new CabinetController(); + + + const promise = controller.deleteCabinet.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 204, next); + } 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('/cabinet/:cabinetName/drawer', + ...(fetchMiddlewares(CabinetController)), + ...(fetchMiddlewares(CabinetController.prototype.listDrawer)), + + function CabinetController_listDrawer(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new CabinetController(); + + + const promise = controller.listDrawer.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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.post('/cabinet/:cabinetName/drawer', + ...(fetchMiddlewares(CabinetController)), + ...(fetchMiddlewares(CabinetController.prototype.createDrawer)), + + function CabinetController_createDrawer(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":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 + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new CabinetController(); + + + const promise = controller.createDrawer.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 204, next); + } 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.post('/file', + upload.single('file'), + ...(fetchMiddlewares(FileController)), + ...(fetchMiddlewares(FileController.prototype.uploadFile)), + + function FileController_uploadFile(request: any, response: any, next: any) { + const args = { + file: {"in":"formData","name":"file","required":true,"dataType":"file"}, + desc: {"in":"formData","name":"desc","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new FileController(); + + + const promise = controller.uploadFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } 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('/folder', + ...(fetchMiddlewares(FolderController)), + ...(fetchMiddlewares(FolderController.prototype.listFolder)), + + function FolderController_listFolder(request: any, response: any, next: any) { + const args = { + cabinet: {"in":"query","name":"cabinet","required":true,"dataType":"string"}, + drawer: {"in":"query","name":"drawer","required":true,"dataType":"string"}, + path: {"in":"query","name":"path","dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new FolderController(); + + + const promise = controller.listFolder.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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.post('/folder', + ...(fetchMiddlewares(FolderController)), + ...(fetchMiddlewares(FolderController.prototype.createFolder)), + + function FolderController_createFolder(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, + cabinet: {"in":"query","name":"cabinet","required":true,"dataType":"string"}, + drawer: {"in":"query","name":"drawer","required":true,"dataType":"string"}, + path: {"in":"query","name":"path","dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new FolderController(); + + + const promise = controller.createFolder.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 201, next); + } 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.put('/folder', + ...(fetchMiddlewares(FolderController)), + ...(fetchMiddlewares(FolderController.prototype.editFolder)), + + function FolderController_editFolder(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, + cabinet: {"in":"query","name":"cabinet","required":true,"dataType":"string"}, + drawer: {"in":"query","name":"drawer","required":true,"dataType":"string"}, + path: {"in":"query","name":"path","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new FolderController(); + + + const promise = controller.editFolder.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 204, next); + } 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 + + function authenticateMiddleware(security: TsoaRoute.Security[] = []) { + return async function runAuthenticationMiddleware(request: any, _response: any, next: any) { + + // 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 + + // keep track of failed auth attempts so we can hand back the most + // recent one. This behavior was previously existing so preserving it + // here + const failedAttempts: any[] = []; + const pushAndRethrow = (error: any) => { + failedAttempts.push(error); + throw error; + }; + + const secMethodOrPromises: Promise[] = []; + for (const secMethod of security) { + if (Object.keys(secMethod).length > 1) { + const secMethodAndPromises: Promise[] = []; + + for (const name in secMethod) { + secMethodAndPromises.push( + expressAuthentication(request, name, secMethod[name]) + .catch(pushAndRethrow) + ); + } + + // 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 + + secMethodOrPromises.push(Promise.all(secMethodAndPromises) + .then(users => { return users[0]; })); + } else { + for (const name in secMethod) { + secMethodOrPromises.push( + expressAuthentication(request, name, secMethod[name]) + .catch(pushAndRethrow) + ); + } + } + } + + // 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 + + try { + request['user'] = await promiseAny.call(Promise, secMethodOrPromises); + next(); + } + catch(err) { + // Show most recent error as response + const error = failedAttempts.pop(); + error.status = error.status || 401; + next(error); + } + + // 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 + + function isController(object: any): object is Controller { + return 'getHeaders' in object && 'getStatus' in object && 'setStatus' in object; + } + + function promiseHandler(controllerObj: any, promise: any, response: any, successStatus: any, next: any) { + return Promise.resolve(promise) + .then((data: any) => { + let statusCode = successStatus; + let headers; + if (isController(controllerObj)) { + headers = controllerObj.getHeaders(); + statusCode = controllerObj.getStatus() || statusCode; + } + + // 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 + + returnHandler(response, statusCode, data, headers) + }) + .catch((error: any) => next(error)); + } + + // 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 + + function returnHandler(response: any, statusCode?: number, data?: any, headers: any = {}) { + if (response.headersSent) { + return; + } + Object.keys(headers).forEach((name: string) => { + response.set(name, headers[name]); + }); + if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') { + response.status(statusCode || 200) + data.pipe(response); + } else if (data !== null && data !== undefined) { + response.status(statusCode || 200).json(data); + } else { + response.status(statusCode || 204).end(); + } + } + + // 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 + + function responder(response: any): TsoaResponse { + return function(status, data, headers) { + returnHandler(response, status, data, headers); + }; + }; + + // 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 + + function getValidatedArgs(args: any, request: any, response: any): any[] { + const fieldErrors: FieldErrors = {}; + const values = Object.keys(args).map((key) => { + const name = args[key].name; + switch (args[key].in) { + case 'request': + return request; + case 'query': + return validationService.ValidateParam(args[key], request.query[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'queries': + return validationService.ValidateParam(args[key], request.query, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'path': + return validationService.ValidateParam(args[key], request.params[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'header': + return validationService.ValidateParam(args[key], request.header(name), name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'body': + return validationService.ValidateParam(args[key], request.body, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'body-prop': + return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, 'body.', {"noImplicitAdditionalProperties":"throw-on-extras"}); + case 'formData': + if (args[key].dataType === 'file') { + return validationService.ValidateParam(args[key], request.file, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + } else if (args[key].dataType === 'array' && args[key].array.dataType === 'file') { + return validationService.ValidateParam(args[key], request.files, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + } else { + return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); + } + case 'res': + return responder(response); + } + }); + + if (Object.keys(fieldErrors).length > 0) { + throw new ValidateError(fieldErrors, ''); + } + return values; + } + + // 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/Prototype/server/src/storage/index.ts b/Prototype/server/src/storage/index.ts new file mode 100644 index 0000000..ca2a279 --- /dev/null +++ b/Prototype/server/src/storage/index.ts @@ -0,0 +1,11 @@ +import * as Minio from "minio"; + +const minioClient = new Minio.Client({ + endPoint: process.env.MINIO_HOST ?? "localhost", + port: +(process.env.MINIO_PORT || 9000), + useSSL: false, + accessKey: process.env.MINIO_ACCESS_KEY ?? "", + secretKey: process.env.MINIO_SECRET_KEY ?? "", +}); + +export default minioClient; diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json new file mode 100644 index 0000000..f0866ae --- /dev/null +++ b/Prototype/server/src/swagger.json @@ -0,0 +1,477 @@ +{ + "components": { + "examples": {}, + "headers": {}, + "parameters": {}, + "requestBodies": {}, + "responses": {}, + "schemas": { + "EhrFolder": { + "properties": { + "pathname": { + "type": "string" + }, + "name": { + "type": "string" + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "createdBy": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + } + }, + "required": [ + "pathname", + "name", + "createdAt", + "createdBy" + ], + "type": "object", + "additionalProperties": false + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "apiKey", + "name": "Authorization", + "description": "Keycloak Bearer Token", + "in": "header" + } + } + }, + "info": { + "title": "BMA EHR - Test Service API", + "version": "0.0.1", + "description": "Best practice for initialize express project", + "license": { + "name": "by Frappet", + "url": "https://frappet.com" + } + }, + "openapi": "3.0.0", + "paths": { + "/cabinet": { + "get": { + "operationId": "ListCabinet", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/EhrFolder" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "Cabinet" + ], + "security": [], + "parameters": [] + }, + "post": { + "operationId": "CreateCabinet", + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "Cabinet" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + } + } + } + } + }, + "/cabinet/{cabinetName}": { + "put": { + "operationId": "EditCabinet", + "responses": { + "204": { + "description": "Success" + } + }, + "tags": [ + "Cabinet" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + } + } + } + }, + "delete": { + "operationId": "DeleteCabinet", + "responses": { + "204": { + "description": "", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "tags": [ + "Cabinet" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/cabinet/{cabinetName}/drawer": { + "get": { + "operationId": "ListDrawer", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/EhrFolder" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "Drawer" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "operationId": "CreateDrawer", + "responses": { + "204": { + "description": "" + } + }, + "tags": [ + "Drawer" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + } + } + } + } + }, + "/file": { + "post": { + "operationId": "UploadFile", + "responses": { + "204": { + "description": "No content" + } + }, + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "desc": { + "type": "string" + } + }, + "required": [ + "file", + "desc" + ] + } + } + } + } + } + }, + "/folder": { + "get": { + "operationId": "ListFolder", + "responses": { + "200": { + "description": "List of folder under drawer or under subfolder", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/EhrFolder" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "Folder" + ], + "security": [], + "parameters": [ + { + "in": "query", + "name": "cabinet", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "drawer", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "path", + "required": false, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "operationId": "CreateFolder", + "responses": { + "201": { + "description": "Folder created." + } + }, + "tags": [ + "Folder" + ], + "security": [], + "parameters": [ + { + "in": "query", + "name": "cabinet", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "drawer", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "path", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + } + } + } + }, + "put": { + "operationId": "EditFolder", + "responses": { + "204": { + "description": "Folder name changed." + } + }, + "tags": [ + "Folder" + ], + "security": [], + "parameters": [ + { + "in": "query", + "name": "cabinet", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "drawer", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + } + } + } + } + } + }, + "servers": [ + { + "url": "/api" + } + ], + "basePath": "/api" +} \ No newline at end of file diff --git a/Prototype/server/src/utils/auth.ts b/Prototype/server/src/utils/auth.ts new file mode 100644 index 0000000..407238a --- /dev/null +++ b/Prototype/server/src/utils/auth.ts @@ -0,0 +1,39 @@ +import * as express from "express"; +import { createVerifier } from "fast-jwt"; + +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; + +if (!process.env.PUBLIC_KEY && !process.env.REALM_URL) { + throw new Error("Require public key or realm url."); +} + +const jwtVerify = createVerifier({ + key: async () => { + return `-----BEGIN PUBLIC KEY-----\n${process.env.PUBLIC_KEY}\n-----END PUBLIC KEY-----`; + }, +}); + +export function expressAuthentication( + request: express.Request, + securityName: string, + _scopes?: string[], +) { + return new Promise(async (resolve, reject) => { + if (securityName !== "bearerAuth") reject(new Error("Unknown authentication method.")); + + const token = request.headers["authorization"]?.includes("Bearer ") + ? request.headers["authorization"].split(" ")[1] + : null; + + if (!token) return reject(new HttpError(HttpStatusCode.UNAUTHORIZED, "No token provided.")); + + const payload = await jwtVerify(token).catch((_) => null); + + if (!payload) { + return reject(new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.")); + } + + return resolve(payload); + }); +} diff --git a/Prototype/server/src/utils/minio.ts b/Prototype/server/src/utils/minio.ts new file mode 100644 index 0000000..ed39987 --- /dev/null +++ b/Prototype/server/src/utils/minio.ts @@ -0,0 +1,54 @@ +import { EhrFolder } from "../interfaces/ehr-fs"; +import minioClient from "../storage"; + +/** + * Remove slash at the start and ensure slash at the end of the path + */ +function safePath(path: string) { + return path.replace(/^\/|\/$/g, "") + "/"; +} + +export async function pathExist(path: string): Promise { + return await minioClient + .statObject("ehr", `${safePath(path)}.keep`) + .then((_) => true) + .catch((e) => { + if (e.code === "NotFound") return false; + throw new Error("Object Storage Error"); + }); +} + +export function listFolder(path?: string): Promise { + if (path) path = safePath(path); + + return new Promise((resolve, reject) => { + const folder: EhrFolder[] = []; + + const stream = minioClient.listObjectsV2("ehr", path ?? ""); + + stream.on("data", (v) => { + if (!(v && v.prefix)) return; + + folder.push({ + pathname: v.prefix, + name: v.prefix.slice(path?.length).split("/")[0], + createdAt: "N/A", + createdBy: "N/A", + }); + }); + + stream.on("end", async () => { + for (let i = 0; i < folder.length; i++) { + const stat = await minioClient.statObject("ehr", `${folder[i].pathname}.keep`); + folder[i] = { + ...folder[i], + createdAt: stat.metaData.createdat ?? "N/A", + createdBy: stat.metaData.createdby ?? "N/A", + }; + } + resolve(folder); + }); + + stream.on("error", () => reject(new Error("Object storage error occured."))); + }); +} diff --git a/Prototype/server/static/.gitkeep b/Prototype/server/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Prototype/server/tsconfig.json b/Prototype/server/tsconfig.json new file mode 100644 index 0000000..b62253b --- /dev/null +++ b/Prototype/server/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "rootDir": "src", + "outDir": "dist", + + "strict": true, + + "esModuleInterop": true, + + "resolveJsonModule": true, + + "experimentalDecorators": true, + + "skipLibCheck": true + } +} diff --git a/Prototype/server/tsoa.json b/Prototype/server/tsoa.json new file mode 100644 index 0000000..d62418f --- /dev/null +++ b/Prototype/server/tsoa.json @@ -0,0 +1,34 @@ +{ + "entryFile": "src/app.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "controllerPathGlobs": ["src/**/*Controller.ts"], + "spec": { + "specVersion": 3, + "outputDirectory": "src", + "basePath": "/api", + "spec": { + "info": { + "title": "BMA EHR - Test Service API", + "version": "0.0.1", + "description": "Best practice for initialize express project", + "license": { + "name": "by Frappet", + "url": "https://frappet.com" + } + }, + "basePath": "/api" + }, + "securityDefinitions": { + "bearerAuth": { + "type": "apiKey", + "name": "Authorization", + "description": "Keycloak Bearer Token", + "in": "header" + } + } + }, + "routes": { + "routesDir": "src", + "authenticationModule": "src/utils/auth.ts" + } +} From ea40c177963cdb43ef716fb51300e74c61dbde01 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 11:09:47 +0700 Subject: [PATCH 02/39] feat: update/delete drawer Also included subfolder and file --- .../src/controllers/cabinetController.ts | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/Prototype/server/src/controllers/cabinetController.ts b/Prototype/server/src/controllers/cabinetController.ts index 43ffa6a..13bd6f9 100644 --- a/Prototype/server/src/controllers/cabinetController.ts +++ b/Prototype/server/src/controllers/cabinetController.ts @@ -2,7 +2,6 @@ import { Body, Controller, Delete, - Example, Get, Path, Post, @@ -10,7 +9,6 @@ import { Route, Security, SuccessResponse, - Response, Tags, } from "tsoa"; import * as Minio from "minio"; @@ -130,4 +128,61 @@ export class CabinetController extends Controller { this.setStatus(HttpStatusCode.CREATED); return; } + + @Put("/{cabinetName}/drawer/{drawerName}") + @Tags("Drawer") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "Success") + public async editDrawer( + @Path() cabinetName: string, + @Path() drawerName: string, + @Body() body: { name: string }, + ): Promise { + const fullpath = `${cabinetName}/${drawerName}/`; + + return new Promise((resolve, reject) => { + const stream = minioClient.listObjectsV2("ehr", fullpath, true); + + stream.on("data", (v) => { + if (!(v && v.name)) return; + + const destination = `${cabinetName}/${body.name}/${v.name.slice(fullpath.length)}`; + const source = `/ehr/${v.name}`; + const cond = new Minio.CopyConditions(); + + minioClient.copyObject("ehr", destination, source, cond, (e) => { + if (e) { + return reject(new Error("Failed to move.")); + } + return minioClient.removeObject("ehr", v.name); + }); + }); + + stream.on("end", () => { + this.setStatus(HttpStatusCode.NO_CONTENT); + resolve(); + }); + stream.on("error", () => reject(new Error("Object storage error occured."))); + }); + } + + @Delete("/{cabinetName}/drawer/{drawerName}") + @Tags("Drawer") + @SuccessResponse(HttpStatusCode.OK) + public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) { + return new Promise((resolve, reject) => { + const objects: string[] = []; + const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/${drawerName}/`, true); + + stream.on("data", (v) => { + if (!(v && v.name)) return; + + objects.push(v.name); + }); + + stream.on("close", () => minioClient.removeObjects("ehr", objects)); + stream.on("error", () => reject(new Error("Object storage error occured."))); + + resolve(true); + }); + } } From a3b8f8e5b4035606d881dcc35a26d22d6398db16 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 11:17:00 +0700 Subject: [PATCH 03/39] chore: updated generated route and swagger --- Prototype/server/src/routes.ts | 53 +++++++++++++++++++ Prototype/server/src/swagger.json | 85 +++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index e647224..a26e976 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -191,6 +191,59 @@ export function RegisterRoutes(app: Router) { } }); // 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.put('/cabinet/:cabinetName/drawer/:drawerName', + ...(fetchMiddlewares(CabinetController)), + ...(fetchMiddlewares(CabinetController.prototype.editDrawer)), + + function CabinetController_editDrawer(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":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 + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new CabinetController(); + + + const promise = controller.editDrawer.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 204, next); + } 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.delete('/cabinet/:cabinetName/drawer/:drawerName', + ...(fetchMiddlewares(CabinetController)), + ...(fetchMiddlewares(CabinetController.prototype.deleteDrawer)), + + function CabinetController_deleteDrawer(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new CabinetController(); + + + const promise = controller.deleteDrawer.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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.post('/file', upload.single('file'), ...(fetchMiddlewares(FileController)), diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index f0866ae..062d81d 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -270,6 +270,91 @@ } } }, + "/cabinet/{cabinetName}/drawer/{drawerName}": { + "put": { + "operationId": "EditDrawer", + "responses": { + "204": { + "description": "Success" + } + }, + "tags": [ + "Drawer" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + } + } + } + }, + "delete": { + "operationId": "DeleteDrawer", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "tags": [ + "Drawer" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, "/file": { "post": { "operationId": "UploadFile", From afcc51209548bc08ea3d2674f3960812bc7be6af Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 11:48:53 +0700 Subject: [PATCH 04/39] chore: dotenv example --- Prototype/server/.env.example | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Prototype/server/.env.example diff --git a/Prototype/server/.env.example b/Prototype/server/.env.example new file mode 100644 index 0000000..32e22a9 --- /dev/null +++ b/Prototype/server/.env.example @@ -0,0 +1,8 @@ +PUBLIC_KEY= +REALM_URL= +PORT= + +MINIO_HOST= +MINIO_PORT= +MINIO_ACCESS_KEY= +MINIO_SECRET_KEY= From a34eab7da57b4ded7ae9cc833694041cc96c07bd Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 14:43:23 +0700 Subject: [PATCH 05/39] chore: separate drawer controller --- .../src/controllers/cabinetController.ts | 107 +----------------- .../src/controllers/drawerController.ts | 99 ++++++++++++++++ 2 files changed, 102 insertions(+), 104 deletions(-) create mode 100644 Prototype/server/src/controllers/drawerController.ts diff --git a/Prototype/server/src/controllers/cabinetController.ts b/Prototype/server/src/controllers/cabinetController.ts index 13bd6f9..344eb82 100644 --- a/Prototype/server/src/controllers/cabinetController.ts +++ b/Prototype/server/src/controllers/cabinetController.ts @@ -1,23 +1,10 @@ -import { - Body, - Controller, - Delete, - Get, - Path, - Post, - Put, - Route, - Security, - SuccessResponse, - Tags, -} from "tsoa"; +import { Body, Controller, Delete, Get, Path, Post, Put, Route, SuccessResponse, Tags } from "tsoa"; import * as Minio from "minio"; import minioClient from "../storage"; import { EhrFolder } from "../interfaces/ehr-fs"; import HttpStatusCode from "../interfaces/http-status"; -import HttpError from "../interfaces/http-error"; -import { listFolder, pathExist } from "../utils/minio"; +import { listFolder } from "../utils/minio"; @Route("cabinet") export class CabinetController extends Controller { @@ -30,7 +17,6 @@ export class CabinetController extends Controller { @Post("/") @Tags("Cabinet") - @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) public async createCabinet(@Body() body: { name: string }) { const uploaded = await minioClient @@ -95,94 +81,7 @@ export class CabinetController extends Controller { stream.on("close", () => minioClient.removeObjects("ehr", objects)); stream.on("error", () => reject(new Error("Object storage error occured."))); - resolve(true); - }); - } - - @Get("/{cabinetName}/drawer") - @Tags("Drawer") - @SuccessResponse(HttpStatusCode.OK) - public listDrawer(@Path() cabinetName: string) { - return listFolder(`${cabinetName}/`); - } - - @Post("/{cabinetName}/drawer") - @Tags("Drawer") - @SuccessResponse(HttpStatusCode.NO_CONTENT) - public async createDrawer(@Path() cabinetName: string, @Body() body: { name: string }) { - if (!(await pathExist(`${cabinetName}/`))) { - throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet cannot be found."); - } - - const uploaded = await minioClient - .putObject("ehr", `${cabinetName}/${body.name}/.keep`, "", 0, { - createdAt: new Date().toISOString(), - createdBy: "SomeUser", - }) - .catch((e) => console.error(e)); - - if (!uploaded) { - throw new Error("Object storage error occured."); - } - - this.setStatus(HttpStatusCode.CREATED); - return; - } - - @Put("/{cabinetName}/drawer/{drawerName}") - @Tags("Drawer") - @SuccessResponse(HttpStatusCode.NO_CONTENT, "Success") - public async editDrawer( - @Path() cabinetName: string, - @Path() drawerName: string, - @Body() body: { name: string }, - ): Promise { - const fullpath = `${cabinetName}/${drawerName}/`; - - return new Promise((resolve, reject) => { - const stream = minioClient.listObjectsV2("ehr", fullpath, true); - - stream.on("data", (v) => { - if (!(v && v.name)) return; - - const destination = `${cabinetName}/${body.name}/${v.name.slice(fullpath.length)}`; - const source = `/ehr/${v.name}`; - const cond = new Minio.CopyConditions(); - - minioClient.copyObject("ehr", destination, source, cond, (e) => { - if (e) { - return reject(new Error("Failed to move.")); - } - return minioClient.removeObject("ehr", v.name); - }); - }); - - stream.on("end", () => { - this.setStatus(HttpStatusCode.NO_CONTENT); - resolve(); - }); - stream.on("error", () => reject(new Error("Object storage error occured."))); - }); - } - - @Delete("/{cabinetName}/drawer/{drawerName}") - @Tags("Drawer") - @SuccessResponse(HttpStatusCode.OK) - public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) { - return new Promise((resolve, reject) => { - const objects: string[] = []; - const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/${drawerName}/`, true); - - stream.on("data", (v) => { - if (!(v && v.name)) return; - - objects.push(v.name); - }); - - stream.on("close", () => minioClient.removeObjects("ehr", objects)); - stream.on("error", () => reject(new Error("Object storage error occured."))); - - resolve(true); + resolve(this.setStatus(HttpStatusCode.NO_CONTENT)); }); } } diff --git a/Prototype/server/src/controllers/drawerController.ts b/Prototype/server/src/controllers/drawerController.ts new file mode 100644 index 0000000..c66dec5 --- /dev/null +++ b/Prototype/server/src/controllers/drawerController.ts @@ -0,0 +1,99 @@ +import { Body, Controller, Delete, Get, Path, Post, Put, Route, SuccessResponse, Tags } from "tsoa"; +import * as Minio from "minio"; +import minioClient from "../storage"; + +import HttpStatusCode from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; +import { listFolder, pathExist } from "../utils/minio"; + +@Route("/cabinet") +export class DrawerController extends Controller { + @Get("/{cabinetName}/drawer") + @Tags("Drawer") + @SuccessResponse(HttpStatusCode.OK) + public listDrawer(@Path() cabinetName: string) { + return listFolder(`${cabinetName}/`); + } + + @Post("/{cabinetName}/drawer") + @Tags("Drawer") + @SuccessResponse(HttpStatusCode.CREATED) + public async createDrawer(@Path() cabinetName: string, @Body() body: { name: string }) { + if (!(await pathExist(`${cabinetName}/`))) { + throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet cannot be found."); + } + + const uploaded = await minioClient + .putObject("ehr", `${cabinetName}/${body.name}/.keep`, "", 0, { + createdAt: new Date().toISOString(), + createdBy: "SomeUser", + }) + .catch((e) => console.error(e)); + + if (!uploaded) { + throw new Error("Object storage error occured."); + } + + return this.setStatus(HttpStatusCode.CREATED); + } + + @Put("/{cabinetName}/drawer/{drawerName}") + @Tags("Drawer") + @SuccessResponse(HttpStatusCode.NO_CONTENT) + public async editDrawer( + @Path() cabinetName: string, + @Path() drawerName: string, + @Body() body: { name: string }, + ): Promise { + const fullpath = `${cabinetName}/${drawerName}/`; + + if (!(await pathExist(fullpath))) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "Resource cannot be found."); + } + + return new Promise((resolve, reject) => { + const stream = minioClient.listObjectsV2("ehr", fullpath, true); + + stream.on("data", (v) => { + if (!(v && v.name)) return; + + const destination = `${cabinetName}/${body.name}/${v.name.slice(fullpath.length)}`; + const source = `/ehr/${v.name}`; + const cond = new Minio.CopyConditions(); + + minioClient.copyObject("ehr", destination, source, cond, (e) => { + if (e) { + return reject(new Error("Failed to move.")); + } + return minioClient.removeObject("ehr", v.name); + }); + }); + + stream.on("end", () => { + resolve(this.setStatus(HttpStatusCode.NO_CONTENT)); + }); + stream.on("error", () => reject(new Error("Object storage error occured."))); + }); + } + + @Delete("/{cabinetName}/drawer/{drawerName}") + @Tags("Drawer") + @SuccessResponse(HttpStatusCode.NO_CONTENT) + public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) { + return new Promise((resolve, reject) => { + const objects: string[] = []; + const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/${drawerName}/`, true); + + stream.on("data", (v) => { + if (!(v && v.name)) return; + + objects.push(v.name); + }); + + stream.on("close", () => minioClient.removeObjects("ehr", objects)); + stream.on("error", () => reject(new Error("Object storage error occured."))); + + resolve(true); + }); + } +} From 9cb38ba9f6848875857977ae1077e6860ae39b21 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 14:43:51 +0700 Subject: [PATCH 06/39] feat: folder controller --- .../src/controllers/folderController.ts | 135 ++++++++++++------ 1 file changed, 92 insertions(+), 43 deletions(-) diff --git a/Prototype/server/src/controllers/folderController.ts b/Prototype/server/src/controllers/folderController.ts index 3cd0380..b845972 100644 --- a/Prototype/server/src/controllers/folderController.ts +++ b/Prototype/server/src/controllers/folderController.ts @@ -1,22 +1,34 @@ -import { Body, Controller, Get, Post, Put, Query, Route, SuccessResponse, Tags } from "tsoa"; +import { + Body, + Controller, + Delete, + Get, + Path, + Post, + Put, + Query, + Route, + SuccessResponse, + Tags, +} from "tsoa"; +import * as Minio from "minio"; + import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; import { listFolder, pathExist } from "../utils/minio"; import { EhrFolder } from "../interfaces/ehr-fs"; import minioClient from "../storage"; -@Route("/folder") +@Route("/cabinet") export class FolderController extends Controller { - @Get("/") + @Get("/{cabinetName}/drawer/{drawerName}/folder") @Tags("Folder") - @SuccessResponse(HttpStatusCode.OK, "List of folder under drawer or under subfolder") + @SuccessResponse(HttpStatusCode.OK) public async listFolder( - @Query() cabinet: string, - @Query() drawer: string, - @Query() path?: string, + @Path() cabinetName: string, + @Path() drawerName: string, ): Promise { - const fullpath = - [cabinet, drawer, path?.replace(/^\/|\/$/g, "")].filter((v) => !!v).join("/") + "/"; + const fullpath = [cabinetName, drawerName].join("/") + "/"; if (!(await pathExist(fullpath))) { throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); @@ -25,24 +37,20 @@ export class FolderController extends Controller { return listFolder(fullpath); } - @Post("/") + @Post("/{cabinetName}/drawer/{drawerName}/folder") @Tags("Folder") - @SuccessResponse(HttpStatusCode.CREATED, "Folder created.") + @SuccessResponse(HttpStatusCode.CREATED) public async createFolder( @Body() body: { name: string }, - @Query() cabinet: string, - @Query() drawer: string, - @Query() path?: string, + @Path() cabinetName: string, + @Path() drawerName: string, ) { - const fullpath = - [cabinet, drawer, path?.replace(/^\/|\/$/g, "")].filter((v) => !!v).join("/") + "/"; - - if (!(await pathExist(fullpath))) { - throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Provided path does not exist."); + if (!(await pathExist(`${cabinetName}/${drawerName}/`))) { + throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet or drawer cannot be found."); } const uploaded = await minioClient - .putObject("ehr", `${fullpath}${body.name}/.keep`, "", 0, { + .putObject("ehr", `${cabinetName}/${drawerName}/${body.name}/.keep`, "", 0, { createdAt: new Date().toISOString(), createdBy: "SomeUser", }) @@ -52,39 +60,80 @@ export class FolderController extends Controller { throw new Error("Object storage error occured."); } - this.setStatus(HttpStatusCode.CREATED); - return; + return this.setStatus(HttpStatusCode.CREATED); } - @Put("/") + @Put("/{cabinetName}/drawer/{drawerName}/folder/{folderName}") @Tags("Folder") - @SuccessResponse(HttpStatusCode.NO_CONTENT, "Folder name changed.") + @SuccessResponse(HttpStatusCode.NO_CONTENT) public async editFolder( @Body() body: { name: string }, - @Query() cabinet: string, - @Query() drawer: string, - @Query() path: string, + @Query() cabinetName: string, + @Query() drawerName: string, + @Query() folderName: string, ) { - const fullpath = - [cabinet, drawer, path.replace(/^\/|\/$/g, "")].filter((v) => !!v).join("/") + "/"; + const fullpath = [cabinetName, drawerName, folderName].join("/") + "/"; if (!(await pathExist(fullpath))) { - throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Provided path does not exist."); + throw new HttpError( + HttpStatusCode.PRECONDITION_FAILED, + "Provided resource location does not exist.", + ); } - // TODO: Recursive get object and move - // const uploaded = await minioClient - // .putObject("ehr", `${fullpath}${body.name}/.keep`, "", 0, { - // createdAt: new Date().toISOString(), - // createdBy: "SomeUser", - // }) - // .catch((e) => console.error(e)); - // - // if (!uploaded) { - // throw new Error("Object storage error occured."); - // } + return new Promise((resolve, reject) => { + const stream = minioClient.listObjectsV2("ehr", fullpath, true); - this.setStatus(HttpStatusCode.CREATED); - return; + stream.on("data", (v) => { + if (!(v && v.name)) return; + + const destination = `${cabinetName}/${drawerName}/${body.name}/${v.name.slice( + fullpath.length, + )}`; + const source = `/ehr/${v.name}`; + const cond = new Minio.CopyConditions(); + + minioClient.copyObject("ehr", destination, source, cond, (e) => { + if (e) { + return reject(new Error("Failed to move.")); + } + return minioClient.removeObject("ehr", v.name); + }); + }); + + stream.on("end", () => { + resolve(this.setStatus(HttpStatusCode.NO_CONTENT)); + }); + stream.on("error", () => reject(new Error("Object storage error occured."))); + }); + } + + @Delete("/{cabinetName}/drawer/{drawerName}/folder/{folderName}") + @Tags("Folder") + @SuccessResponse(HttpStatusCode.NO_CONTENT) + public async deleteFolder( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + ) { + return new Promise((resolve, reject) => { + const objects: string[] = []; + const stream = minioClient.listObjectsV2( + "ehr", + `${cabinetName}/${drawerName}/${folderName}`, + true, + ); + + stream.on("data", (v) => { + if (!(v && v.name)) return; + + objects.push(v.name); + }); + + stream.on("close", () => minioClient.removeObjects("ehr", objects)); + stream.on("error", () => reject(new Error("Object storage error occured."))); + + resolve(this.setStatus(HttpStatusCode.NO_CONTENT)); + }); } } From c3c7a0ad2756e076948260793bdd97ce0db4243a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 14:44:30 +0700 Subject: [PATCH 07/39] chore: update generated route and swagger --- Prototype/server/src/routes.ts | 146 +++++++++------------ Prototype/server/src/swagger.json | 204 +++++++++++++++++++----------- 2 files changed, 188 insertions(+), 162 deletions(-) diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index a26e976..412e75d 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -5,6 +5,8 @@ import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, H // 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 { CabinetController } from './controllers/cabinetController'; // 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 { DrawerController } from './controllers/drawerController'; +// 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 { FileController } from './controllers/fileController'; // 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 { FolderController } from './controllers/folderController'; @@ -64,7 +66,6 @@ export function RegisterRoutes(app: Router) { }); // 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.post('/cabinet', - authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.createCabinet)), @@ -141,10 +142,10 @@ export function RegisterRoutes(app: Router) { }); // 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('/cabinet/:cabinetName/drawer', - ...(fetchMiddlewares(CabinetController)), - ...(fetchMiddlewares(CabinetController.prototype.listDrawer)), + ...(fetchMiddlewares(DrawerController)), + ...(fetchMiddlewares(DrawerController.prototype.listDrawer)), - function CabinetController_listDrawer(request: any, response: any, next: any) { + function DrawerController_listDrawer(request: any, response: any, next: any) { const args = { cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, }; @@ -155,7 +156,7 @@ export function RegisterRoutes(app: Router) { try { validatedArgs = getValidatedArgs(args, request, response); - const controller = new CabinetController(); + const controller = new DrawerController(); const promise = controller.listDrawer.apply(controller, validatedArgs as any); @@ -166,10 +167,10 @@ export function RegisterRoutes(app: Router) { }); // 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.post('/cabinet/:cabinetName/drawer', - ...(fetchMiddlewares(CabinetController)), - ...(fetchMiddlewares(CabinetController.prototype.createDrawer)), + ...(fetchMiddlewares(DrawerController)), + ...(fetchMiddlewares(DrawerController.prototype.createDrawer)), - function CabinetController_createDrawer(request: any, response: any, next: any) { + function DrawerController_createDrawer(request: any, response: any, next: any) { const args = { cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, @@ -181,21 +182,21 @@ export function RegisterRoutes(app: Router) { try { validatedArgs = getValidatedArgs(args, request, response); - const controller = new CabinetController(); + const controller = new DrawerController(); const promise = controller.createDrawer.apply(controller, validatedArgs as any); - promiseHandler(controller, promise, response, 204, next); + promiseHandler(controller, promise, response, 201, next); } 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.put('/cabinet/:cabinetName/drawer/:drawerName', - ...(fetchMiddlewares(CabinetController)), - ...(fetchMiddlewares(CabinetController.prototype.editDrawer)), + ...(fetchMiddlewares(DrawerController)), + ...(fetchMiddlewares(DrawerController.prototype.editDrawer)), - function CabinetController_editDrawer(request: any, response: any, next: any) { + function DrawerController_editDrawer(request: any, response: any, next: any) { const args = { cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, @@ -208,7 +209,7 @@ export function RegisterRoutes(app: Router) { try { validatedArgs = getValidatedArgs(args, request, response); - const controller = new CabinetController(); + const controller = new DrawerController(); const promise = controller.editDrawer.apply(controller, validatedArgs as any); @@ -219,10 +220,10 @@ export function RegisterRoutes(app: Router) { }); // 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.delete('/cabinet/:cabinetName/drawer/:drawerName', - ...(fetchMiddlewares(CabinetController)), - ...(fetchMiddlewares(CabinetController.prototype.deleteDrawer)), + ...(fetchMiddlewares(DrawerController)), + ...(fetchMiddlewares(DrawerController.prototype.deleteDrawer)), - function CabinetController_deleteDrawer(request: any, response: any, next: any) { + function DrawerController_deleteDrawer(request: any, response: any, next: any) { const args = { cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, @@ -234,17 +235,17 @@ export function RegisterRoutes(app: Router) { try { validatedArgs = getValidatedArgs(args, request, response); - const controller = new CabinetController(); + const controller = new DrawerController(); const promise = controller.deleteDrawer.apply(controller, validatedArgs as any); - promiseHandler(controller, promise, response, 200, next); + promiseHandler(controller, promise, response, 204, next); } 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.post('/file', + app.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName', upload.single('file'), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.uploadFile)), @@ -253,6 +254,9 @@ export function RegisterRoutes(app: Router) { const args = { file: {"in":"formData","name":"file","required":true,"dataType":"file"}, desc: {"in":"formData","name":"desc","required":true,"dataType":"string"}, + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, }; // 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 @@ -265,21 +269,20 @@ export function RegisterRoutes(app: Router) { const promise = controller.uploadFile.apply(controller, validatedArgs as any); - promiseHandler(controller, promise, response, undefined, next); + promiseHandler(controller, promise, response, 204, next); } 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('/folder', + app.get('/cabinet/:cabinetName/drawer/:drawerName/folder', ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.listFolder)), function FolderController_listFolder(request: any, response: any, next: any) { const args = { - cabinet: {"in":"query","name":"cabinet","required":true,"dataType":"string"}, - drawer: {"in":"query","name":"drawer","required":true,"dataType":"string"}, - path: {"in":"query","name":"path","dataType":"string"}, + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, }; // 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 @@ -298,16 +301,15 @@ export function RegisterRoutes(app: Router) { } }); // 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.post('/folder', + app.post('/cabinet/:cabinetName/drawer/:drawerName/folder', ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.createFolder)), function FolderController_createFolder(request: any, response: any, next: any) { const args = { body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, - cabinet: {"in":"query","name":"cabinet","required":true,"dataType":"string"}, - drawer: {"in":"query","name":"drawer","required":true,"dataType":"string"}, - path: {"in":"query","name":"path","dataType":"string"}, + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, }; // 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 @@ -326,16 +328,16 @@ export function RegisterRoutes(app: Router) { } }); // 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.put('/folder', + app.put('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName', ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.editFolder)), function FolderController_editFolder(request: any, response: any, next: any) { const args = { body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, - cabinet: {"in":"query","name":"cabinet","required":true,"dataType":"string"}, - drawer: {"in":"query","name":"drawer","required":true,"dataType":"string"}, - path: {"in":"query","name":"path","required":true,"dataType":"string"}, + cabinetName: {"in":"query","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"query","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"query","name":"folderName","required":true,"dataType":"string"}, }; // 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 @@ -354,68 +356,36 @@ export function RegisterRoutes(app: Router) { } }); // 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName', + ...(fetchMiddlewares(FolderController)), + ...(fetchMiddlewares(FolderController.prototype.deleteFolder)), - // 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 - - function authenticateMiddleware(security: TsoaRoute.Security[] = []) { - return async function runAuthenticationMiddleware(request: any, _response: any, next: any) { - - // 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 - - // keep track of failed auth attempts so we can hand back the most - // recent one. This behavior was previously existing so preserving it - // here - const failedAttempts: any[] = []; - const pushAndRethrow = (error: any) => { - failedAttempts.push(error); - throw error; + function FolderController_deleteFolder(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, }; - const secMethodOrPromises: Promise[] = []; - for (const secMethod of security) { - if (Object.keys(secMethod).length > 1) { - const secMethodAndPromises: Promise[] = []; - - for (const name in secMethod) { - secMethodAndPromises.push( - expressAuthentication(request, name, secMethod[name]) - .catch(pushAndRethrow) - ); - } - - // 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 - - secMethodOrPromises.push(Promise.all(secMethodAndPromises) - .then(users => { return users[0]; })); - } else { - for (const name in secMethod) { - secMethodOrPromises.push( - expressAuthentication(request, name, secMethod[name]) - .catch(pushAndRethrow) - ); - } - } - } - // 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 { - request['user'] = await promiseAny.call(Promise, secMethodOrPromises); - next(); - } - catch(err) { - // Show most recent error as response - const error = failedAttempts.pop(); - error.status = error.status || 401; - next(error); - } + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new FolderController(); + + + const promise = controller.deleteFolder.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 204, next); + } 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/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index 062d81d..1d55d71 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -101,11 +101,7 @@ "tags": [ "Cabinet" ], - "security": [ - { - "bearerAuth": [] - } - ], + "security": [], "parameters": [], "requestBody": { "required": true, @@ -232,7 +228,7 @@ "post": { "operationId": "CreateDrawer", "responses": { - "204": { + "201": { "description": "" } }, @@ -275,7 +271,7 @@ "operationId": "EditDrawer", "responses": { "204": { - "description": "Success" + "description": "" } }, "tags": [ @@ -322,7 +318,7 @@ "delete": { "operationId": "DeleteDrawer", "responses": { - "200": { + "204": { "description": "", "content": { "application/json": { @@ -355,16 +351,44 @@ ] } }, - "/file": { + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}": { "post": { "operationId": "UploadFile", "responses": { "204": { - "description": "No content" + "description": "" } }, + "tags": [ + "File" + ], "security": [], - "parameters": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { @@ -388,22 +412,15 @@ } } } - } - }, - "/folder": { - "get": { - "operationId": "ListFolder", + }, + "put": { + "operationId": "EditFolder", "responses": { - "200": { - "description": "List of folder under drawer or under subfolder", + "204": { + "description": "", "content": { "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/EhrFolder" - }, - "type": "array" - } + "schema": {} } } } @@ -415,7 +432,7 @@ "parameters": [ { "in": "query", - "name": "cabinet", + "name": "cabinetName", "required": true, "schema": { "type": "string" @@ -423,7 +440,7 @@ }, { "in": "query", - "name": "drawer", + "name": "drawerName", "required": true, "schema": { "type": "string" @@ -431,49 +448,11 @@ }, { "in": "query", - "name": "path", - "required": false, - "schema": { - "type": "string" - } - } - ] - }, - "post": { - "operationId": "CreateFolder", - "responses": { - "201": { - "description": "Folder created." - } - }, - "tags": [ - "Folder" - ], - "security": [], - "parameters": [ - { - "in": "query", - "name": "cabinet", + "name": "folderName", "required": true, "schema": { "type": "string" } - }, - { - "in": "query", - "name": "drawer", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "path", - "required": false, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -495,11 +474,16 @@ } } }, - "put": { - "operationId": "EditFolder", + "delete": { + "operationId": "DeleteFolder", "responses": { "204": { - "description": "Folder name changed." + "description": "", + "content": { + "application/json": { + "schema": {} + } + } } }, "tags": [ @@ -508,24 +492,96 @@ "security": [], "parameters": [ { - "in": "query", - "name": "cabinet", + "in": "path", + "name": "cabinetName", "required": true, "schema": { "type": "string" } }, { - "in": "query", - "name": "drawer", + "in": "path", + "name": "drawerName", "required": true, "schema": { "type": "string" } }, { - "in": "query", - "name": "path", + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/cabinet/{cabinetName}/drawer/{drawerName}/folder": { + "get": { + "operationId": "ListFolder", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/EhrFolder" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "Folder" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "operationId": "CreateFolder", + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "Folder" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", "required": true, "schema": { "type": "string" From 083e984c89569b7a3d31d9f9dc6b53b7a501e283 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:17:12 +0700 Subject: [PATCH 08/39] feat: subfolder --- .../src/controllers/subFolderController.ts | 146 +++++++++++ Prototype/server/src/routes.ts | 114 +++++++++ Prototype/server/src/swagger.json | 228 ++++++++++++++++++ 3 files changed, 488 insertions(+) create mode 100644 Prototype/server/src/controllers/subFolderController.ts diff --git a/Prototype/server/src/controllers/subFolderController.ts b/Prototype/server/src/controllers/subFolderController.ts new file mode 100644 index 0000000..a8f518d --- /dev/null +++ b/Prototype/server/src/controllers/subFolderController.ts @@ -0,0 +1,146 @@ +import { + Body, + Controller, + Delete, + Get, + Path, + Post, + Put, + Query, + Route, + SuccessResponse, + Tags, +} from "tsoa"; +import * as Minio from "minio"; + +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { listFolder, pathExist } from "../utils/minio"; +import { EhrFolder } from "../interfaces/ehr-fs"; +import minioClient from "../storage"; + +@Route("/cabinet") +export class SubFolderController extends Controller { + @Get("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") + @Tags("SubFolder") + @SuccessResponse(HttpStatusCode.OK) + public async listFolder( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + ): Promise { + const fullpath = [cabinetName, drawerName, folderName].join("/") + "/"; + + if (!(await pathExist(fullpath))) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); + } + + return listFolder(fullpath); + } + + @Post("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") + @Tags("SubFolder") + @SuccessResponse(HttpStatusCode.CREATED) + public async createFolder( + @Body() body: { name: string }, + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + ) { + if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}`))) { + throw new HttpError( + HttpStatusCode.PRECONDITION_FAILED, + "Cabinet, drawer or folder cannot be found.", + ); + } + + const uploaded = await minioClient + .putObject("ehr", `${cabinetName}/${drawerName}/${folderName}/${body.name}/.keep`, "", 0, { + createdAt: new Date().toISOString(), + createdBy: "SomeUser", + }) + .catch((e) => console.error(e)); + + if (!uploaded) { + throw new Error("Object storage error occured."); + } + + return this.setStatus(HttpStatusCode.CREATED); + } + + @Put("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}") + @Tags("SubFolder") + @SuccessResponse(HttpStatusCode.NO_CONTENT) + public async editFolder( + @Body() body: { name: string }, + @Query() cabinetName: string, + @Query() drawerName: string, + @Query() folderName: string, + @Query() subFolderName: string, + ) { + const fullpath = [cabinetName, drawerName, folderName, subFolderName].join("/") + "/"; + + if (!(await pathExist(fullpath))) { + throw new HttpError( + HttpStatusCode.PRECONDITION_FAILED, + "Provided resource location does not exist.", + ); + } + + return new Promise((resolve, reject) => { + const stream = minioClient.listObjectsV2("ehr", fullpath, true); + + stream.on("data", (v) => { + if (!(v && v.name)) return; + + const destination = `${cabinetName}/${drawerName}/${folderName}/${body.name}/${v.name.slice( + fullpath.length, + )}`; + const source = `/ehr/${v.name}`; + const cond = new Minio.CopyConditions(); + + minioClient.copyObject("ehr", destination, source, cond, (e) => { + if (e) { + return reject(new Error("Failed to move.")); + } + return minioClient.removeObject("ehr", v.name); + }); + }); + + stream.on("end", () => { + resolve(this.setStatus(HttpStatusCode.NO_CONTENT)); + }); + stream.on("error", () => reject(new Error("Object storage error occured."))); + }); + } + + @Delete("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}") + @Tags("SubFolder") + @SuccessResponse(HttpStatusCode.NO_CONTENT) + public async deleteFolder( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() subFolderName: string, + ) { + return new Promise((resolve, reject) => { + const objects: string[] = []; + const stream = minioClient.listObjectsV2( + "ehr", + `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, + true, + ); + + stream.on("data", (v) => { + if (!(v && v.name)) return; + + objects.push(v.name); + }); + + stream.on("close", () => minioClient.removeObjects("ehr", objects)); + stream.on("error", () => reject(new Error("Object storage error occured."))); + + resolve(this.setStatus(HttpStatusCode.NO_CONTENT)); + }); + } +} diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 412e75d..12097c0 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -10,6 +10,8 @@ import { DrawerController } from './controllers/drawerController'; import { FileController } from './controllers/fileController'; // 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 { FolderController } from './controllers/folderController'; +// 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 { SubFolderController } from './controllers/subFolderController'; import { expressAuthentication } from './utils/auth'; // @ts-ignore - no great way to install types from subpackage const promiseAny = require('promise.any'); @@ -376,6 +378,118 @@ export function RegisterRoutes(app: Router) { const controller = new FolderController(); + const promise = controller.deleteFolder.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 204, next); + } 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder', + ...(fetchMiddlewares(SubFolderController)), + ...(fetchMiddlewares(SubFolderController.prototype.listFolder)), + + function SubFolderController_listFolder(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new SubFolderController(); + + + const promise = controller.listFolder.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder', + ...(fetchMiddlewares(SubFolderController)), + ...(fetchMiddlewares(SubFolderController.prototype.createFolder)), + + function SubFolderController_createFolder(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new SubFolderController(); + + + const promise = controller.createFolder.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 201, next); + } 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.put('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName', + ...(fetchMiddlewares(SubFolderController)), + ...(fetchMiddlewares(SubFolderController.prototype.editFolder)), + + function SubFolderController_editFolder(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, + cabinetName: {"in":"query","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"query","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"query","name":"folderName","required":true,"dataType":"string"}, + subFolderName: {"in":"query","name":"subFolderName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new SubFolderController(); + + + const promise = controller.editFolder.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 204, next); + } 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName', + ...(fetchMiddlewares(SubFolderController)), + ...(fetchMiddlewares(SubFolderController.prototype.deleteFolder)), + + function SubFolderController_deleteFolder(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + subFolderName: {"in":"path","name":"subFolderName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new SubFolderController(); + + const promise = controller.deleteFolder.apply(controller, validatedArgs as any); promiseHandler(controller, promise, response, 204, next); } catch (err) { diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index 1d55d71..049ab8a 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -607,6 +607,234 @@ } } } + }, + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder": { + "get": { + "operationId": "ListFolder", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/EhrFolder" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "SubFolder" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "operationId": "CreateFolder", + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "SubFolder" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + } + } + } + } + }, + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}": { + "put": { + "operationId": "EditFolder", + "responses": { + "204": { + "description": "", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "tags": [ + "SubFolder" + ], + "security": [], + "parameters": [ + { + "in": "query", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "subFolderName", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + } + } + } + }, + "delete": { + "operationId": "DeleteFolder", + "responses": { + "204": { + "description": "", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "tags": [ + "SubFolder" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "subFolderName", + "required": true, + "schema": { + "type": "string" + } + } + ] + } } }, "servers": [ From b5b3b02d091631c037980c8e3a112b8d3788c3e9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:43:07 +0700 Subject: [PATCH 09/39] feat: protect route --- .../src/controllers/cabinetController.ts | 25 +++++++++++++++--- .../src/controllers/drawerController.ts | 26 ++++++++++++++++--- .../src/controllers/folderController.ts | 8 +++++- .../src/controllers/subFolderController.ts | 9 ++++++- 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/Prototype/server/src/controllers/cabinetController.ts b/Prototype/server/src/controllers/cabinetController.ts index 344eb82..7e64364 100644 --- a/Prototype/server/src/controllers/cabinetController.ts +++ b/Prototype/server/src/controllers/cabinetController.ts @@ -1,4 +1,17 @@ -import { Body, Controller, Delete, Get, Path, Post, Put, Route, SuccessResponse, Tags } from "tsoa"; +import { + Body, + Controller, + Delete, + Get, + Path, + Post, + Put, + Route, + Security, + SuccessResponse, + Tags, + Request, +} from "tsoa"; import * as Minio from "minio"; import minioClient from "../storage"; @@ -17,12 +30,16 @@ export class CabinetController extends Controller { @Post("/") @Tags("Cabinet") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) - public async createCabinet(@Body() body: { name: string }) { + public async createCabinet( + @Request() request: { user: { preferred_username: string } }, + @Body() body: { name: string }, + ) { const uploaded = await minioClient .putObject("ehr", `${body.name}/.keep`, "", 0, { createdAt: new Date().toISOString(), - createdBy: "SomeUser", + createdBy: request.user.preferred_username, }) .catch((e) => console.error(e)); @@ -33,6 +50,7 @@ export class CabinetController extends Controller { @Put("/{cabinetName}") @Tags("Cabinet") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT, "Success") public async editCabinet( @Path() cabinetName: string, @@ -66,6 +84,7 @@ export class CabinetController extends Controller { @Delete("/{cabinetName}") @Tags("Cabinet") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) public async deleteCabinet(@Path() cabinetName: string) { return new Promise((resolve, reject) => { diff --git a/Prototype/server/src/controllers/drawerController.ts b/Prototype/server/src/controllers/drawerController.ts index c66dec5..2100bc1 100644 --- a/Prototype/server/src/controllers/drawerController.ts +++ b/Prototype/server/src/controllers/drawerController.ts @@ -1,4 +1,17 @@ -import { Body, Controller, Delete, Get, Path, Post, Put, Route, SuccessResponse, Tags } from "tsoa"; +import { + Body, + Controller, + Delete, + Get, + Path, + Post, + Put, + Request, + Route, + Security, + SuccessResponse, + Tags, +} from "tsoa"; import * as Minio from "minio"; import minioClient from "../storage"; @@ -17,8 +30,13 @@ export class DrawerController extends Controller { @Post("/{cabinetName}/drawer") @Tags("Drawer") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) - public async createDrawer(@Path() cabinetName: string, @Body() body: { name: string }) { + public async createDrawer( + @Request() request: { user: { preferred_username: string } }, + @Path() cabinetName: string, + @Body() body: { name: string }, + ) { if (!(await pathExist(`${cabinetName}/`))) { throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet cannot be found."); } @@ -26,7 +44,7 @@ export class DrawerController extends Controller { const uploaded = await minioClient .putObject("ehr", `${cabinetName}/${body.name}/.keep`, "", 0, { createdAt: new Date().toISOString(), - createdBy: "SomeUser", + createdBy: request.user.preferred_username, }) .catch((e) => console.error(e)); @@ -39,6 +57,7 @@ export class DrawerController extends Controller { @Put("/{cabinetName}/drawer/{drawerName}") @Tags("Drawer") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) public async editDrawer( @Path() cabinetName: string, @@ -78,6 +97,7 @@ export class DrawerController extends Controller { @Delete("/{cabinetName}/drawer/{drawerName}") @Tags("Drawer") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) { return new Promise((resolve, reject) => { diff --git a/Prototype/server/src/controllers/folderController.ts b/Prototype/server/src/controllers/folderController.ts index b845972..3d3104d 100644 --- a/Prototype/server/src/controllers/folderController.ts +++ b/Prototype/server/src/controllers/folderController.ts @@ -7,7 +7,9 @@ import { Post, Put, Query, + Request, Route, + Security, SuccessResponse, Tags, } from "tsoa"; @@ -39,8 +41,10 @@ export class FolderController extends Controller { @Post("/{cabinetName}/drawer/{drawerName}/folder") @Tags("Folder") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) public async createFolder( + @Request() request: { user: { preferred_username: string } }, @Body() body: { name: string }, @Path() cabinetName: string, @Path() drawerName: string, @@ -52,7 +56,7 @@ export class FolderController extends Controller { const uploaded = await minioClient .putObject("ehr", `${cabinetName}/${drawerName}/${body.name}/.keep`, "", 0, { createdAt: new Date().toISOString(), - createdBy: "SomeUser", + createdBy: request.user.preferred_username, }) .catch((e) => console.error(e)); @@ -65,6 +69,7 @@ export class FolderController extends Controller { @Put("/{cabinetName}/drawer/{drawerName}/folder/{folderName}") @Tags("Folder") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) public async editFolder( @Body() body: { name: string }, @@ -110,6 +115,7 @@ export class FolderController extends Controller { @Delete("/{cabinetName}/drawer/{drawerName}/folder/{folderName}") @Tags("Folder") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) public async deleteFolder( @Path() cabinetName: string, diff --git a/Prototype/server/src/controllers/subFolderController.ts b/Prototype/server/src/controllers/subFolderController.ts index a8f518d..0c96cb7 100644 --- a/Prototype/server/src/controllers/subFolderController.ts +++ b/Prototype/server/src/controllers/subFolderController.ts @@ -7,7 +7,9 @@ import { Post, Put, Query, + Request, Route, + Security, SuccessResponse, Tags, } from "tsoa"; @@ -23,6 +25,7 @@ import minioClient from "../storage"; export class SubFolderController extends Controller { @Get("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") @Tags("SubFolder") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) public async listFolder( @Path() cabinetName: string, @@ -40,8 +43,10 @@ export class SubFolderController extends Controller { @Post("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") @Tags("SubFolder") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) public async createFolder( + @Request() request: { user: { preferred_username: string } }, @Body() body: { name: string }, @Path() cabinetName: string, @Path() drawerName: string, @@ -57,7 +62,7 @@ export class SubFolderController extends Controller { const uploaded = await minioClient .putObject("ehr", `${cabinetName}/${drawerName}/${folderName}/${body.name}/.keep`, "", 0, { createdAt: new Date().toISOString(), - createdBy: "SomeUser", + createdBy: request.user.preferred_username, }) .catch((e) => console.error(e)); @@ -70,6 +75,7 @@ export class SubFolderController extends Controller { @Put("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}") @Tags("SubFolder") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) public async editFolder( @Body() body: { name: string }, @@ -116,6 +122,7 @@ export class SubFolderController extends Controller { @Delete("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}") @Tags("SubFolder") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) public async deleteFolder( @Path() cabinetName: string, From e48dcb913388bc0d7827d6e7a21687c971a40ed5 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:43:20 +0700 Subject: [PATCH 10/39] chore: update generated route and swagger --- Prototype/server/src/routes.ts | 82 ++++++++++++++++++++++++++- Prototype/server/src/swagger.json | 94 ++++++++++++++++++++++++++----- 2 files changed, 160 insertions(+), 16 deletions(-) diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 12097c0..681dea5 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -68,11 +68,13 @@ export function RegisterRoutes(app: Router) { }); // 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.post('/cabinet', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.createCabinet)), function CabinetController_createCabinet(request: any, response: any, next: any) { const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, }; @@ -93,6 +95,7 @@ export function RegisterRoutes(app: Router) { }); // 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.put('/cabinet/:cabinetName', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.editCabinet)), @@ -119,6 +122,7 @@ export function RegisterRoutes(app: Router) { }); // 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.delete('/cabinet/:cabinetName', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.deleteCabinet)), @@ -169,11 +173,13 @@ export function RegisterRoutes(app: Router) { }); // 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.post('/cabinet/:cabinetName/drawer', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.createDrawer)), function DrawerController_createDrawer(request: any, response: any, next: any) { const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, }; @@ -195,6 +201,7 @@ export function RegisterRoutes(app: Router) { }); // 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.put('/cabinet/:cabinetName/drawer/:drawerName', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.editDrawer)), @@ -222,6 +229,7 @@ export function RegisterRoutes(app: Router) { }); // 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.delete('/cabinet/:cabinetName/drawer/:drawerName', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.deleteDrawer)), @@ -254,8 +262,12 @@ export function RegisterRoutes(app: Router) { function FileController_uploadFile(request: any, response: any, next: any) { const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, file: {"in":"formData","name":"file","required":true,"dataType":"file"}, - desc: {"in":"formData","name":"desc","required":true,"dataType":"string"}, + title: {"in":"formData","name":"title","required":true,"dataType":"string"}, + description: {"in":"formData","name":"description","required":true,"dataType":"string"}, + keywords: {"in":"formData","name":"keywords","required":true,"dataType":"string"}, + categories: {"in":"formData","name":"categories","required":true,"dataType":"string"}, cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, @@ -304,11 +316,13 @@ export function RegisterRoutes(app: Router) { }); // 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.post('/cabinet/:cabinetName/drawer/:drawerName/folder', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.createFolder)), function FolderController_createFolder(request: any, response: any, next: any) { const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, @@ -331,6 +345,7 @@ export function RegisterRoutes(app: Router) { }); // 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.put('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.editFolder)), @@ -359,6 +374,7 @@ export function RegisterRoutes(app: Router) { }); // 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.deleteFolder)), @@ -386,6 +402,7 @@ export function RegisterRoutes(app: Router) { }); // 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.listFolder)), @@ -413,11 +430,13 @@ export function RegisterRoutes(app: Router) { }); // 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.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.createFolder)), function SubFolderController_createFolder(request: any, response: any, next: any) { const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true}}}, cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, @@ -441,6 +460,7 @@ export function RegisterRoutes(app: Router) { }); // 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.put('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.editFolder)), @@ -470,6 +490,7 @@ export function RegisterRoutes(app: Router) { }); // 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.deleteFolder)), @@ -501,6 +522,65 @@ export function RegisterRoutes(app: Router) { // 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 + + function authenticateMiddleware(security: TsoaRoute.Security[] = []) { + return async function runAuthenticationMiddleware(request: any, _response: any, next: any) { + + // 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 + + // keep track of failed auth attempts so we can hand back the most + // recent one. This behavior was previously existing so preserving it + // here + const failedAttempts: any[] = []; + const pushAndRethrow = (error: any) => { + failedAttempts.push(error); + throw error; + }; + + const secMethodOrPromises: Promise[] = []; + for (const secMethod of security) { + if (Object.keys(secMethod).length > 1) { + const secMethodAndPromises: Promise[] = []; + + for (const name in secMethod) { + secMethodAndPromises.push( + expressAuthentication(request, name, secMethod[name]) + .catch(pushAndRethrow) + ); + } + + // 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 + + secMethodOrPromises.push(Promise.all(secMethodAndPromises) + .then(users => { return users[0]; })); + } else { + for (const name in secMethod) { + secMethodOrPromises.push( + expressAuthentication(request, name, secMethod[name]) + .catch(pushAndRethrow) + ); + } + } + } + + // 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 + + try { + request['user'] = await promiseAny.call(Promise, secMethodOrPromises); + next(); + } + catch(err) { + // Show most recent error as response + const error = failedAttempts.pop(); + error.status = error.status || 401; + next(error); + } + + // 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 function isController(object: any): object is Controller { diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index 049ab8a..8918c87 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -101,7 +101,11 @@ "tags": [ "Cabinet" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [], "requestBody": { "required": true, @@ -134,7 +138,11 @@ "tags": [ "Cabinet" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -179,7 +187,11 @@ "tags": [ "Cabinet" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -235,7 +247,11 @@ "tags": [ "Drawer" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -277,7 +293,11 @@ "tags": [ "Drawer" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -330,7 +350,11 @@ "tags": [ "Drawer" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -400,13 +424,25 @@ "type": "string", "format": "binary" }, - "desc": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "keywords": { + "type": "string" + }, + "categories": { "type": "string" } }, "required": [ "file", - "desc" + "title", + "description", + "keywords", + "categories" ] } } @@ -428,7 +464,11 @@ "tags": [ "Folder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "query", @@ -489,7 +529,11 @@ "tags": [ "Folder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -569,7 +613,11 @@ "tags": [ "Folder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -629,7 +677,11 @@ "tags": [ "SubFolder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -667,7 +719,11 @@ "tags": [ "SubFolder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -730,7 +786,11 @@ "tags": [ "SubFolder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "query", @@ -799,7 +859,11 @@ "tags": [ "SubFolder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", From 8fdee84b97b8849d662aa6633156c9abba913b49 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:21:52 +0700 Subject: [PATCH 11/39] feat: upload file and elasticsearch index with metadata --- .../server/src/controllers/fileController.ts | 78 +++++++++++++++---- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index c43947a..1c4dca2 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -1,24 +1,72 @@ -import { Controller, FormField, Post, Route, UploadedFile } from "tsoa"; -import minioClient from "../storage"; +import { + Controller, + FormField, + Path, + Post, + Request, + Route, + Security, + SuccessResponse, + Tags, + UploadedFile, +} from "tsoa"; import esClient from "../elasticsearch"; +import minioClient from "../storage"; +import HttpStatusCode from "../interfaces/http-status"; +import { pathExist } from "../utils/minio"; +import HttpError from "../interfaces/http-error"; -@Route("/file") +@Route("/cabinet") export class FileController extends Controller { - @Post("/") - public async uploadFile(@UploadedFile() file: Express.Multer.File, @FormField() desc: string) { + @Post("/{cabinetName}/drawer/{drawerName}/folder/{folderName}") + @Tags("File") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.CREATED) + public async uploadFile( + @Request() request: any, + @UploadedFile() file: Express.Multer.File, + @FormField() title: string, + @FormField() description: string, + @FormField() keywords: string, + @FormField() categories: string, + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + ) { const filename = Buffer.from(file.originalname, "latin1").toString("utf-8"); + const pathname = `${cabinetName}/${drawerName}/${folderName}/${filename}`; - console.log( - esClient.search({ - query: { - match_all: {}, + if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/`))) { + throw new HttpError( + HttpStatusCode.PRECONDITION_FAILED, + "Cabinet, drawer or folder cannot be found.", + ); + } + + const info = await minioClient + .putObject("ehr", pathname, file.buffer, file.size, { + "Content-Type": file.mimetype, + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + }) + .catch(() => new Error("Object storage error occured.")); + + if (info) { + await esClient.index({ + pipeline: "attachment", + index: "ehr-api-client", + document: { + data: Buffer.from(file.buffer).toString("base64"), + path: pathname, + title, + description, + keywords, + categories, }, - }), - ); + op_type: "index", + }); + } - minioClient.putObject("ehr", `test_upload_file/${filename}`, file.buffer, file.size, { - "Content-Type": file.mimetype, - }); - return; + return this.setStatus(HttpStatusCode.CREATED); } } From 478c89ea9225e80ab9da645099410e36693cdc5b Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:07:57 +0700 Subject: [PATCH 12/39] feat: handle exists file --- .../server/src/controllers/fileController.ts | 62 ++++- Prototype/server/src/interfaces/ehr-fs.ts | 4 +- Prototype/server/src/middlewares/exception.ts | 11 + Prototype/server/src/routes.ts | 36 ++- Prototype/server/src/swagger.json | 252 ++++++++++++------ 5 files changed, 266 insertions(+), 99 deletions(-) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index 1c4dca2..c050dca 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -15,20 +15,21 @@ import minioClient from "../storage"; import HttpStatusCode from "../interfaces/http-status"; import { pathExist } from "../utils/minio"; import HttpError from "../interfaces/http-error"; +import { EhrFile } from "../interfaces/ehr-fs"; @Route("/cabinet") export class FileController extends Controller { - @Post("/{cabinetName}/drawer/{drawerName}/folder/{folderName}") + @Post("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file") @Tags("File") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) public async uploadFile( - @Request() request: any, + @Request() request: { user: { preferred_username: string } }, @UploadedFile() file: Express.Multer.File, @FormField() title: string, @FormField() description: string, - @FormField() keywords: string, - @FormField() categories: string, + @FormField() keyword: string, + @FormField() category: string, @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @@ -49,21 +50,58 @@ export class FileController extends Controller { createdAt: new Date().toISOString(), createdBy: request.user.preferred_username, }) - .catch(() => new Error("Object storage error occured.")); + .catch((e) => console.error(e)); - if (info) { + if (!info) throw new Error("Object storage error occured."); + + const search = await esClient.search }>({ + index: "ehr-api-client", + query: { + match: { + pathname: pathname, + }, + }, + }); + + const exist = search.hits.hits.find((v) => v._source?.pathname === pathname); + + const metadata: Partial = { + pathname, + fileName: filename, + fileSize: file.size, + fileType: file.mimetype, + title: title, + description: description, + category: category.split(","), + keyword: keyword.split(","), + }; + + if (!exist) { await esClient.index({ pipeline: "attachment", index: "ehr-api-client", document: { data: Buffer.from(file.buffer).toString("base64"), - path: pathname, - title, - description, - keywords, - categories, + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username, + ...metadata, + }, + }); + } else { + await esClient.delete({ index: exist._index, id: exist._id }); + await esClient.index({ + pipeline: "attachment", + index: "ehr-api-client", + document: { + data: Buffer.from(file.buffer).toString("base64"), + createdAt: exist._source?.createdAt, + createdBy: exist._source?.createdBy, + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username, + ...metadata, }, - op_type: "index", }); } diff --git a/Prototype/server/src/interfaces/ehr-fs.ts b/Prototype/server/src/interfaces/ehr-fs.ts index 5916436..919cbcc 100644 --- a/Prototype/server/src/interfaces/ehr-fs.ts +++ b/Prototype/server/src/interfaces/ehr-fs.ts @@ -19,9 +19,11 @@ export interface EhrFile { pathname: string; fileName: string; - fileSize: string; + fileSize: number; fileType: string; + title: string; + description: string; category: string[]; keyword: string[]; diff --git a/Prototype/server/src/middlewares/exception.ts b/Prototype/server/src/middlewares/exception.ts index dcf36c3..ccab12a 100644 --- a/Prototype/server/src/middlewares/exception.ts +++ b/Prototype/server/src/middlewares/exception.ts @@ -1,6 +1,7 @@ import { NextFunction, Request, Response } from "express"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; +import { ValidateError } from "tsoa"; function errorHandler(error: Error, _req: Request, res: Response, _next: NextFunction) { if (error instanceof HttpError) { @@ -10,6 +11,16 @@ function errorHandler(error: Error, _req: Request, res: Response, _next: NextFun }); } + if (error instanceof ValidateError) { + return res.status(error.status).json({ + status: HttpStatusCode.UNPROCESSABLE_ENTITY, + message: "Validation error(s).", + detail: error.fields, + }); + } + + console.error(error); + return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR).json({ status: HttpStatusCode.INTERNAL_SERVER_ERROR, message: error.message, diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 681dea5..3fa6d7a 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -255,7 +255,8 @@ export function RegisterRoutes(app: Router) { } }); // 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.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName', + app.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file', + authenticateMiddleware([{"bearerAuth":[]}]), upload.single('file'), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.uploadFile)), @@ -266,8 +267,8 @@ export function RegisterRoutes(app: Router) { file: {"in":"formData","name":"file","required":true,"dataType":"file"}, title: {"in":"formData","name":"title","required":true,"dataType":"string"}, description: {"in":"formData","name":"description","required":true,"dataType":"string"}, - keywords: {"in":"formData","name":"keywords","required":true,"dataType":"string"}, - categories: {"in":"formData","name":"categories","required":true,"dataType":"string"}, + keyword: {"in":"formData","name":"keyword","required":true,"dataType":"string"}, + category: {"in":"formData","name":"category","required":true,"dataType":"string"}, cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, @@ -283,7 +284,34 @@ export function RegisterRoutes(app: Router) { const promise = controller.uploadFile.apply(controller, validatedArgs as any); - promiseHandler(controller, promise, response, 204, next); + promiseHandler(controller, promise, response, 201, next); + } 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file', + ...(fetchMiddlewares(FileController)), + ...(fetchMiddlewares(FileController.prototype.getFile)), + + function FileController_getFile(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new FileController(); + + + const promise = controller.getFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); } catch (err) { return next(err); } diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index 8918c87..e0bb1ae 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -375,18 +375,22 @@ ] } }, - "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}": { + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file": { "post": { "operationId": "UploadFile", "responses": { - "204": { + "201": { "description": "" } }, "tags": [ "File" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -430,10 +434,10 @@ "description": { "type": "string" }, - "keywords": { + "keyword": { "type": "string" }, - "categories": { + "category": { "type": "string" } }, @@ -441,99 +445,69 @@ "file", "title", "description", - "keywords", - "categories" + "keyword", + "category" ] } } } } }, - "put": { - "operationId": "EditFolder", + "get": { + "operationId": "GetFile", "responses": { - "204": { + "200": { "description": "", "content": { "application/json": { - "schema": {} + "schema": { + "items": { + "properties": { + "type": { + "type": "string" + }, + "category": { + "items": { + "type": "string" + }, + "type": "array" + }, + "keyword": { + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "pathname": { + "type": "string" + } + }, + "required": [ + "type", + "category", + "keyword", + "description", + "title", + "pathname" + ], + "type": "object" + }, + "type": "array" + } } } } }, "tags": [ - "Folder" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "in": "query", - "name": "cabinetName", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "drawerName", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "folderName", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - } - } - } - } - }, - "delete": { - "operationId": "DeleteFolder", - "responses": { - "204": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } - } - }, - "tags": [ - "Folder" - ], - "security": [ - { - "bearerAuth": [] - } + "File" ], + "security": [], "parameters": [ { "in": "path", @@ -656,6 +630,120 @@ } } }, + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}": { + "put": { + "operationId": "EditFolder", + "responses": { + "204": { + "description": "", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "tags": [ + "Folder" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + } + } + } + }, + "delete": { + "operationId": "DeleteFolder", + "responses": { + "204": { + "description": "", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "tags": [ + "Folder" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder": { "get": { "operationId": "ListFolder", From 510ad444dfd40bd488a5458ed689108008f919e1 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:08:25 +0700 Subject: [PATCH 13/39] feat: read file list --- .../server/src/controllers/fileController.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index c050dca..7a7107c 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -1,6 +1,7 @@ import { Controller, FormField, + Get, Path, Post, Request, @@ -107,4 +108,43 @@ export class FileController extends Controller { return this.setStatus(HttpStatusCode.CREATED); } + + @Get("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file") + @Tags("File") + @SuccessResponse(HttpStatusCode.OK) + public async getFile( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + ) { + const search = await esClient.search< + EhrFile & { + attachment: Record; + } + >({ + index: "ehr-api-client", + query: { + prefix: { + pathname: `${cabinetName}/${drawerName}/${folderName}/`, + }, + }, + }); + + const records = search.hits.hits + .map((v) => { + const { pathname, title, description, keyword, category, attachment } = v._source!; + + return { + pathname, + title, + description, + keyword, + category, + type: attachment["content_type"], + }; + }) + .filter((v) => !!v); + + return records; + } } From 82ee1ef7f7a798890045a7911be73b1f23e17a11 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:50:18 +0700 Subject: [PATCH 14/39] fix: missing metadata field --- .../server/src/controllers/fileController.ts | 22 ++++- Prototype/server/src/routes.ts | 28 +++++++ Prototype/server/src/swagger.json | 80 +++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index 7a7107c..7999145 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -130,9 +130,23 @@ export class FileController extends Controller { }, }); + // Use flatMap for return type only. Filter does not change type after filter out undefined or null const records = search.hits.hits .map((v) => { - const { pathname, title, description, keyword, category, attachment } = v._source!; + if (!v._source) return; + + const { + pathname, + title, + description, + keyword, + category, + attachment, + createdAt, + createdBy, + updatedAt, + updatedBy, + } = v._source; return { pathname, @@ -141,9 +155,13 @@ export class FileController extends Controller { keyword, category, type: attachment["content_type"], + createdAt, + createdBy, + updatedAt, + updatedBy, }; }) - .filter((v) => !!v); + .flatMap((v) => (v ? [v] : [])); return records; } diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 3fa6d7a..14d6579 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -317,6 +317,34 @@ export function RegisterRoutes(app: Router) { } }); // 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName', + ...(fetchMiddlewares(FileController)), + ...(fetchMiddlewares(FileController.prototype.deleteFile)), + + function FileController_deleteFile(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new FileController(); + + + const promise = controller.deleteFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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('/cabinet/:cabinetName/drawer/:drawerName/folder', ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.listFolder)), diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index e0bb1ae..fa19899 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -463,6 +463,34 @@ "schema": { "items": { "properties": { + "updatedBy": { + "type": "string" + }, + "updatedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "createdBy": { + "type": "string" + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, "type": { "type": "string" }, @@ -489,6 +517,10 @@ } }, "required": [ + "updatedBy", + "updatedAt", + "createdBy", + "createdAt", "type", "category", "keyword", @@ -536,6 +568,54 @@ ] } }, + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}": { + "get": { + "operationId": "DeleteFile", + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "File" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "fileName", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder": { "get": { "operationId": "ListFolder", From a0d70fbb38889409561f23c76780db3acb202de6 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:03:28 +0700 Subject: [PATCH 15/39] feat: delete file --- .../server/src/controllers/fileController.ts | 41 +++++++++++++++++++ Prototype/server/src/routes.ts | 2 +- Prototype/server/src/swagger.json | 2 +- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index 7999145..fb47fa4 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -1,5 +1,6 @@ import { Controller, + Delete, FormField, Get, Path, @@ -165,4 +166,44 @@ export class FileController extends Controller { return records; } + + @Delete("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}") + @Tags("File") + @SuccessResponse(HttpStatusCode.OK) + public async deleteFile( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() fileName: string, + ) { + const search = await esClient.search< + EhrFile & { + attachment: Record; + } + >({ + index: "ehr-api-client", + query: { + match: { + pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, + }, + }, + }); + + if (search && search.hits.hits.length === 0) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); + } + + const esResult = await esClient + .delete({ + index: "ehr-api-client", + id: search.hits.hits[0]._id, + }) + .catch((e) => console.error(e)); + + if (!esResult) throw new Error("An error occured, cannot perform this action."); + + await minioClient.removeObject("ehr", `${cabinetName}/${drawerName}/${folderName}/${fileName}`); + + return this.setStatus(HttpStatusCode.NO_CONTENT); + } } diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 14d6579..3c9b377 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -317,7 +317,7 @@ export function RegisterRoutes(app: Router) { } }); // 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName', + app.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName', ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.deleteFile)), diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index fa19899..0878d45 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -569,7 +569,7 @@ } }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}": { - "get": { + "delete": { "operationId": "DeleteFile", "responses": { "200": { From c95ea595ce96581865f29a79f25518668fc02a90 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:04:26 +0700 Subject: [PATCH 16/39] chore: add security to previous commit --- Prototype/server/src/controllers/fileController.ts | 2 ++ Prototype/server/src/routes.ts | 2 ++ Prototype/server/src/swagger.json | 12 ++++++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index fb47fa4..57f3379 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -112,6 +112,7 @@ export class FileController extends Controller { @Get("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file") @Tags("File") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) public async getFile( @Path() cabinetName: string, @@ -169,6 +170,7 @@ export class FileController extends Controller { @Delete("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}") @Tags("File") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) public async deleteFile( @Path() cabinetName: string, diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 3c9b377..749a396 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -291,6 +291,7 @@ export function RegisterRoutes(app: Router) { }); // 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.getFile)), @@ -318,6 +319,7 @@ export function RegisterRoutes(app: Router) { }); // 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.deleteFile)), diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index 0878d45..41ab6e3 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -539,7 +539,11 @@ "tags": [ "File" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -579,7 +583,11 @@ "tags": [ "File" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", From b67a3d1b2274800e45797701c0b7fe3b9899b865 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:13:02 +0700 Subject: [PATCH 17/39] feat: update file --- .../server/src/controllers/fileController.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index 57f3379..3e5081c 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -3,6 +3,7 @@ import { Delete, FormField, Get, + Patch, Path, Post, Request, @@ -168,6 +169,98 @@ export class FileController extends Controller { return records; } + @Patch("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}") + @Tags("File") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK) + public async updateFile( + @Request() request: { user: { preferred_username: string } }, + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() fileName: string, + @UploadedFile() file?: Express.Multer.File, + @FormField() title?: string, + @FormField() description?: string, + @FormField() keyword?: string, + @FormField() category?: string, + ) { + const search = await esClient.search }>({ + index: "ehr-api-client", + query: { + match: { + pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, + }, + }, + }); + + if (search && search.hits.hits.length === 0) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); + } + + const data = search.hits.hits[0]; + + if (!file) { + const esResult = await esClient + .update({ + index: "ehr-api-client", + id: data._id, + doc: { + title, + description, + keyword: keyword?.split(","), + category: category?.split(","), + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username, + }, + }) + .catch((e) => console.error(e)); + + if (!esResult) throw new Error("An error occured, cannot perform this action."); + } else { + const filename = Buffer.from(file.originalname, "latin1").toString("utf-8"); + const pathname = `${cabinetName}/${drawerName}/${folderName}/${filename}`; + + await minioClient.removeObject( + "ehr", + `${cabinetName}/${drawerName}/${folderName}/${fileName}`, + ); + + const info = await minioClient + .putObject("ehr", pathname, file.buffer, file.size, { + "Content-Type": file.mimetype, + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + }) + .catch((e) => console.error(e)); + + if (!info) throw new Error("Object storage error occured."); + + await esClient.delete({ index: data._index, id: data._id }); + await esClient.index({ + pipeline: "attachment", + index: "ehr-api-client", + document: { + data: Buffer.from(file.buffer).toString("base64"), + pathname, + fileName: filename, + fileSize: file.size, + fileType: file.mimetype, + title: title, + description: description, + category: category?.split(","), + keyword: keyword?.split(","), + createdAt: data._source?.createdAt, + createdBy: data._source?.createdBy, + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username, + }, + }); + } + + return this.setStatus(HttpStatusCode.NO_CONTENT); + } + @Delete("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}") @Tags("File") @Security("bearerAuth") From 627567abf600894c4a8ec105bdd9251b864e0240 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:13:19 +0700 Subject: [PATCH 18/39] refactor: return data --- .../server/src/controllers/fileController.ts | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index 3e5081c..34e61fa 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -113,7 +113,6 @@ export class FileController extends Controller { @Get("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file") @Tags("File") - @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) public async getFile( @Path() cabinetName: string, @@ -138,31 +137,9 @@ export class FileController extends Controller { .map((v) => { if (!v._source) return; - const { - pathname, - title, - description, - keyword, - category, - attachment, - createdAt, - createdBy, - updatedAt, - updatedBy, - } = v._source; + const { attachment, ...rest } = v._source; - return { - pathname, - title, - description, - keyword, - category, - type: attachment["content_type"], - createdAt, - createdBy, - updatedAt, - updatedBy, - }; + return rest; }) .flatMap((v) => (v ? [v] : [])); From 44259437f6fcc129cf59762538d8c458d446ea9f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:13:26 +0700 Subject: [PATCH 19/39] chore: updated route --- Prototype/server/src/routes.ts | 37 ++++++++- Prototype/server/src/swagger.json | 133 ++++++++++++++++++++++++------ 2 files changed, 144 insertions(+), 26 deletions(-) diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 749a396..01ceec9 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -291,7 +291,6 @@ export function RegisterRoutes(app: Router) { }); // 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file', - authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.getFile)), @@ -318,6 +317,42 @@ export function RegisterRoutes(app: Router) { } }); // 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.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName', + authenticateMiddleware([{"bearerAuth":[]}]), + upload.single('file'), + ...(fetchMiddlewares(FileController)), + ...(fetchMiddlewares(FileController.prototype.updateFile)), + + function FileController_updateFile(request: any, response: any, next: any) { + const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"}, + file: {"in":"formData","name":"file","dataType":"file"}, + title: {"in":"formData","name":"title","dataType":"string"}, + description: {"in":"formData","name":"description","dataType":"string"}, + keyword: {"in":"formData","name":"keyword","dataType":"string"}, + category: {"in":"formData","name":"category","dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new FileController(); + + + const promise = controller.updateFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName', authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FileController)), diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index 41ab6e3..8cf67eb 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -463,20 +463,6 @@ "schema": { "items": { "properties": { - "updatedBy": { - "type": "string" - }, - "updatedAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date-time" - } - ] - }, "createdBy": { "type": "string" }, @@ -491,16 +477,27 @@ } ] }, - "type": { + "updatedBy": { "type": "string" }, - "category": { + "updatedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "keyword": { "items": { "type": "string" }, "type": "array" }, - "keyword": { + "category": { "items": { "type": "string" }, @@ -512,20 +509,32 @@ "title": { "type": "string" }, + "fileType": { + "type": "string" + }, + "fileSize": { + "type": "number", + "format": "double" + }, + "fileName": { + "type": "string" + }, "pathname": { "type": "string" } }, "required": [ - "updatedBy", - "updatedAt", "createdBy", "createdAt", - "type", - "category", + "updatedBy", + "updatedAt", "keyword", + "category", "description", "title", + "fileType", + "fileSize", + "fileName", "pathname" ], "type": "object" @@ -539,6 +548,46 @@ "tags": [ "File" ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}": { + "patch": { + "operationId": "UpdateFile", + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "File" + ], "security": [ { "bearerAuth": [] @@ -568,11 +617,45 @@ "schema": { "type": "string" } + }, + { + "in": "path", + "name": "fileName", + "required": true, + "schema": { + "type": "string" + } } - ] - } - }, - "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}": { + ], + "requestBody": { + "required": false, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "keyword": { + "type": "string" + }, + "category": { + "type": "string" + } + } + } + } + } + } + }, "delete": { "operationId": "DeleteFile", "responses": { From ba4d615d12ed6d74424c14159d1ad89e6f83aff5 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:54:02 +0700 Subject: [PATCH 20/39] fix: cannot get subfolder without authenticated --- Prototype/server/src/controllers/subFolderController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/Prototype/server/src/controllers/subFolderController.ts b/Prototype/server/src/controllers/subFolderController.ts index 0c96cb7..5e7b68b 100644 --- a/Prototype/server/src/controllers/subFolderController.ts +++ b/Prototype/server/src/controllers/subFolderController.ts @@ -25,7 +25,6 @@ import minioClient from "../storage"; export class SubFolderController extends Controller { @Get("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") @Tags("SubFolder") - @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) public async listFolder( @Path() cabinetName: string, From 11d2ece007f6a35a3aabdf840a3b2db020f1cb9a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:54:19 +0700 Subject: [PATCH 21/39] chore: updated route --- Prototype/server/src/routes.ts | 134 ++++++++++- Prototype/server/src/swagger.json | 370 +++++++++++++++++++++++++++++- 2 files changed, 498 insertions(+), 6 deletions(-) diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 01ceec9..d006624 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -12,6 +12,8 @@ import { FileController } from './controllers/fileController'; import { FolderController } from './controllers/folderController'; // 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 { SubFolderController } from './controllers/subFolderController'; +// 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 { SubFolderFileController } from './controllers/subFolderFileController'; import { expressAuthentication } from './utils/auth'; // @ts-ignore - no great way to install types from subpackage const promiseAny = require('promise.any'); @@ -495,7 +497,6 @@ export function RegisterRoutes(app: Router) { }); // 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder', - authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.listFolder)), @@ -611,6 +612,137 @@ export function RegisterRoutes(app: Router) { } }); // 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.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file', + authenticateMiddleware([{"bearerAuth":[]}]), + upload.single('file'), + ...(fetchMiddlewares(SubFolderFileController)), + ...(fetchMiddlewares(SubFolderFileController.prototype.uploadFile)), + + function SubFolderFileController_uploadFile(request: any, response: any, next: any) { + const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + file: {"in":"formData","name":"file","required":true,"dataType":"file"}, + title: {"in":"formData","name":"title","required":true,"dataType":"string"}, + description: {"in":"formData","name":"description","required":true,"dataType":"string"}, + keyword: {"in":"formData","name":"keyword","required":true,"dataType":"string"}, + category: {"in":"formData","name":"category","required":true,"dataType":"string"}, + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + subFolderName: {"in":"path","name":"subFolderName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new SubFolderFileController(); + + + const promise = controller.uploadFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 201, next); + } 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file', + ...(fetchMiddlewares(SubFolderFileController)), + ...(fetchMiddlewares(SubFolderFileController.prototype.getFile)), + + function SubFolderFileController_getFile(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + subFolderName: {"in":"path","name":"subFolderName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new SubFolderFileController(); + + + const promise = controller.getFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file/:fileName', + authenticateMiddleware([{"bearerAuth":[]}]), + upload.single('file'), + ...(fetchMiddlewares(SubFolderFileController)), + ...(fetchMiddlewares(SubFolderFileController.prototype.updateFile)), + + function SubFolderFileController_updateFile(request: any, response: any, next: any) { + const args = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + subFolderName: {"in":"path","name":"subFolderName","required":true,"dataType":"string"}, + fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"}, + file: {"in":"formData","name":"file","dataType":"file"}, + title: {"in":"formData","name":"title","dataType":"string"}, + description: {"in":"formData","name":"description","dataType":"string"}, + keyword: {"in":"formData","name":"keyword","dataType":"string"}, + category: {"in":"formData","name":"category","dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new SubFolderFileController(); + + + const promise = controller.updateFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file/:fileName', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(fetchMiddlewares(SubFolderFileController)), + ...(fetchMiddlewares(SubFolderFileController.prototype.deleteFile)), + + function SubFolderFileController_deleteFile(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + subFolderName: {"in":"path","name":"subFolderName","required":true,"dataType":"string"}, + fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new SubFolderFileController(); + + + const promise = controller.deleteFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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 diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index 8cf67eb..38ee325 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -936,11 +936,7 @@ "tags": [ "SubFolder" ], - "security": [ - { - "bearerAuth": [] - } - ], + "security": [], "parameters": [ { "in": "path", @@ -1158,6 +1154,370 @@ } ] } + }, + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}/file": { + "post": { + "operationId": "UploadFile", + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "SubFolder File" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "subFolderName", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "keyword": { + "type": "string" + }, + "category": { + "type": "string" + } + }, + "required": [ + "file", + "title", + "description", + "keyword", + "category" + ] + } + } + } + } + }, + "get": { + "operationId": "GetFile", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "items": { + "properties": { + "createdBy": { + "type": "string" + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "updatedBy": { + "type": "string" + }, + "updatedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "keyword": { + "items": { + "type": "string" + }, + "type": "array" + }, + "category": { + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "fileType": { + "type": "string" + }, + "fileSize": { + "type": "number", + "format": "double" + }, + "fileName": { + "type": "string" + }, + "pathname": { + "type": "string" + } + }, + "required": [ + "createdBy", + "createdAt", + "updatedBy", + "updatedAt", + "keyword", + "category", + "description", + "title", + "fileType", + "fileSize", + "fileName", + "pathname" + ], + "type": "object" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "SubFolder File" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "subFolderName", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}/file/{fileName}": { + "patch": { + "operationId": "UpdateFile", + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "SubFolder File" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "subFolderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "fileName", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "keyword": { + "type": "string" + }, + "category": { + "type": "string" + } + } + } + } + } + } + }, + "delete": { + "operationId": "DeleteFile", + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "SubFolder File" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "subFolderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "fileName", + "required": true, + "schema": { + "type": "string" + } + } + ] + } } }, "servers": [ From 344ffe727082ead714c15c60df02de6ce5805e71 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:23:17 +0700 Subject: [PATCH 22/39] chore: response type --- Prototype/server/src/controllers/fileController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index 34e61fa..7382fde 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -118,7 +118,7 @@ export class FileController extends Controller { @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, - ) { + ): Promise { const search = await esClient.search< EhrFile & { attachment: Record; From 0b75f34fa88d76259b5e776bd27b8bd51af42add Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:23:27 +0700 Subject: [PATCH 23/39] chore updated route --- Prototype/server/src/routes.ts | 19 ++++ Prototype/server/src/swagger.json | 156 +++++++++++++++--------------- 2 files changed, 99 insertions(+), 76 deletions(-) diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index d006624..9184477 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -35,6 +35,25 @@ const models: TsoaRoute.Models = { "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 + "EhrFile": { + "dataType": "refObject", + "properties": { + "pathname": {"dataType":"string","required":true}, + "fileName": {"dataType":"string","required":true}, + "fileSize": {"dataType":"double","required":true}, + "fileType": {"dataType":"string","required":true}, + "title": {"dataType":"string","required":true}, + "description": {"dataType":"string","required":true}, + "category": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "keyword": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "updatedAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true}, + "updatedBy": {"dataType":"string","required":true}, + "createdAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true}, + "createdBy": {"dataType":"string","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 validationService = new ValidationService(models); diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index 38ee325..b1d98bc 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -45,6 +45,85 @@ ], "type": "object", "additionalProperties": false + }, + "EhrFile": { + "properties": { + "pathname": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "fileSize": { + "type": "number", + "format": "double" + }, + "fileType": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "category": { + "items": { + "type": "string" + }, + "type": "array" + }, + "keyword": { + "items": { + "type": "string" + }, + "type": "array" + }, + "updatedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "updatedBy": { + "type": "string" + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "createdBy": { + "type": "string" + } + }, + "required": [ + "pathname", + "fileName", + "fileSize", + "fileType", + "title", + "description", + "category", + "keyword", + "updatedAt", + "updatedBy", + "createdAt", + "createdBy" + ], + "type": "object", + "additionalProperties": false } }, "securitySchemes": { @@ -462,82 +541,7 @@ "application/json": { "schema": { "items": { - "properties": { - "createdBy": { - "type": "string" - }, - "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date-time" - } - ] - }, - "updatedBy": { - "type": "string" - }, - "updatedAt": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "date-time" - } - ] - }, - "keyword": { - "items": { - "type": "string" - }, - "type": "array" - }, - "category": { - "items": { - "type": "string" - }, - "type": "array" - }, - "description": { - "type": "string" - }, - "title": { - "type": "string" - }, - "fileType": { - "type": "string" - }, - "fileSize": { - "type": "number", - "format": "double" - }, - "fileName": { - "type": "string" - }, - "pathname": { - "type": "string" - } - }, - "required": [ - "createdBy", - "createdAt", - "updatedBy", - "updatedAt", - "keyword", - "category", - "description", - "title", - "fileType", - "fileSize", - "fileName", - "pathname" - ], - "type": "object" + "$ref": "#/components/schemas/EhrFile" }, "type": "array" } From 911db7802c0d68bf3f11514c14928c7a8fab26a4 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:13:21 +0700 Subject: [PATCH 24/39] chore: update env --- Prototype/server/.env.example | 10 ++++++++-- Prototype/server/src/elasticsearch/index.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Prototype/server/.env.example b/Prototype/server/.env.example index 32e22a9..f2f6554 100644 --- a/Prototype/server/.env.example +++ b/Prototype/server/.env.example @@ -1,8 +1,14 @@ PUBLIC_KEY= REALM_URL= + PORT= -MINIO_HOST= -MINIO_PORT= +MINIO_HOST=localhost +MINIO_PORT=9000 MINIO_ACCESS_KEY= MINIO_SECRET_KEY= + +ELASTICSEARCH_PROTOCOL=http +ELASTICSEARCH_HOST=localhost +ELASTICSEARCH_PORT=9200 + diff --git a/Prototype/server/src/elasticsearch/index.ts b/Prototype/server/src/elasticsearch/index.ts index b1c1552..604bc7a 100644 --- a/Prototype/server/src/elasticsearch/index.ts +++ b/Prototype/server/src/elasticsearch/index.ts @@ -1,7 +1,7 @@ import { Client } from "@elastic/elasticsearch"; const esClient = new Client({ - node: "http://localhost:9200", + node: `${process.env.ELASTICSEARCH_PROTOCOL}://${process.env.ELASTICSEARCH_HOST}:${process.env.ELASTICSEARCH_PORT}`, }); export default esClient; From 1ec2b69f87e5f06e1f9a580bac2d73380988bcde Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:24:46 +0700 Subject: [PATCH 25/39] refactor: route --- .../src/controllers/drawerController.ts | 10 +++---- Prototype/server/src/routes.ts | 26 +++++++++++++++++++ Prototype/server/src/swagger.json | 20 ++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/Prototype/server/src/controllers/drawerController.ts b/Prototype/server/src/controllers/drawerController.ts index 2100bc1..4bf9002 100644 --- a/Prototype/server/src/controllers/drawerController.ts +++ b/Prototype/server/src/controllers/drawerController.ts @@ -19,16 +19,16 @@ import HttpStatusCode from "../interfaces/http-status"; import HttpError from "../interfaces/http-error"; import { listFolder, pathExist } from "../utils/minio"; -@Route("/cabinet") +@Route("/cabinet/{cabinetName}/drawer") export class DrawerController extends Controller { - @Get("/{cabinetName}/drawer") + @Get("/") @Tags("Drawer") @SuccessResponse(HttpStatusCode.OK) public listDrawer(@Path() cabinetName: string) { return listFolder(`${cabinetName}/`); } - @Post("/{cabinetName}/drawer") + @Post("/") @Tags("Drawer") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) @@ -55,7 +55,7 @@ export class DrawerController extends Controller { return this.setStatus(HttpStatusCode.CREATED); } - @Put("/{cabinetName}/drawer/{drawerName}") + @Put("/{drawerName}") @Tags("Drawer") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) @@ -95,7 +95,7 @@ export class DrawerController extends Controller { }); } - @Delete("/{cabinetName}/drawer/{drawerName}") + @Delete("/{drawerName}") @Tags("Drawer") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 9184477..807727b 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -11,6 +11,8 @@ import { FileController } from './controllers/fileController'; // 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 { FolderController } from './controllers/folderController'; // 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 { SearchController } from './controllers/searchController'; +// 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 { SubFolderController } from './controllers/subFolderController'; // 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 { SubFolderFileController } from './controllers/subFolderFileController'; @@ -515,6 +517,30 @@ export function RegisterRoutes(app: Router) { } }); // 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.post('/search', + ...(fetchMiddlewares(SearchController)), + ...(fetchMiddlewares(SearchController.prototype.searchFile)), + + function SearchController_searchFile(request: any, response: any, next: any) { + const args = { + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new SearchController(); + + + const promise = controller.searchFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder', ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.listFolder)), diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index b1d98bc..ed1bf8f 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -919,6 +919,26 @@ ] } }, + "/search": { + "post": { + "operationId": "SearchFile", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "items": {}, + "type": "array" + } + } + } + } + }, + "security": [], + "parameters": [] + } + }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder": { "get": { "operationId": "ListFolder", From 6bf28d11a82d7a6017f3c18afe9b6aad1d51c15d Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:34:38 +0700 Subject: [PATCH 26/39] refactor: route decorators --- Prototype/server/src/controllers/fileController.ts | 10 +++++----- Prototype/server/src/controllers/folderController.ts | 10 +++++----- .../server/src/controllers/subFolderController.ts | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index 7382fde..fe9d1f9 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -20,9 +20,9 @@ import { pathExist } from "../utils/minio"; import HttpError from "../interfaces/http-error"; import { EhrFile } from "../interfaces/ehr-fs"; -@Route("/cabinet") +@Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file") export class FileController extends Controller { - @Post("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file") + @Post("/") @Tags("File") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) @@ -111,7 +111,7 @@ export class FileController extends Controller { return this.setStatus(HttpStatusCode.CREATED); } - @Get("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file") + @Get("/") @Tags("File") @SuccessResponse(HttpStatusCode.OK) public async getFile( @@ -146,7 +146,7 @@ export class FileController extends Controller { return records; } - @Patch("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}") + @Patch("/{fileName}") @Tags("File") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) @@ -238,7 +238,7 @@ export class FileController extends Controller { return this.setStatus(HttpStatusCode.NO_CONTENT); } - @Delete("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}") + @Delete("/{fileName}") @Tags("File") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) diff --git a/Prototype/server/src/controllers/folderController.ts b/Prototype/server/src/controllers/folderController.ts index 3d3104d..d7e4a47 100644 --- a/Prototype/server/src/controllers/folderController.ts +++ b/Prototype/server/src/controllers/folderController.ts @@ -21,9 +21,9 @@ import { listFolder, pathExist } from "../utils/minio"; import { EhrFolder } from "../interfaces/ehr-fs"; import minioClient from "../storage"; -@Route("/cabinet") +@Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder") export class FolderController extends Controller { - @Get("/{cabinetName}/drawer/{drawerName}/folder") + @Get("/") @Tags("Folder") @SuccessResponse(HttpStatusCode.OK) public async listFolder( @@ -39,7 +39,7 @@ export class FolderController extends Controller { return listFolder(fullpath); } - @Post("/{cabinetName}/drawer/{drawerName}/folder") + @Post("/") @Tags("Folder") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) @@ -67,7 +67,7 @@ export class FolderController extends Controller { return this.setStatus(HttpStatusCode.CREATED); } - @Put("/{cabinetName}/drawer/{drawerName}/folder/{folderName}") + @Put("/{folderName}") @Tags("Folder") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) @@ -113,7 +113,7 @@ export class FolderController extends Controller { }); } - @Delete("/{cabinetName}/drawer/{drawerName}/folder/{folderName}") + @Delete("/{folderName}") @Tags("Folder") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) diff --git a/Prototype/server/src/controllers/subFolderController.ts b/Prototype/server/src/controllers/subFolderController.ts index 5e7b68b..f72a2d4 100644 --- a/Prototype/server/src/controllers/subFolderController.ts +++ b/Prototype/server/src/controllers/subFolderController.ts @@ -21,9 +21,9 @@ import { listFolder, pathExist } from "../utils/minio"; import { EhrFolder } from "../interfaces/ehr-fs"; import minioClient from "../storage"; -@Route("/cabinet") +@Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") export class SubFolderController extends Controller { - @Get("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") + @Get("/") @Tags("SubFolder") @SuccessResponse(HttpStatusCode.OK) public async listFolder( @@ -40,7 +40,7 @@ export class SubFolderController extends Controller { return listFolder(fullpath); } - @Post("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") + @Post("/") @Tags("SubFolder") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) @@ -72,7 +72,7 @@ export class SubFolderController extends Controller { return this.setStatus(HttpStatusCode.CREATED); } - @Put("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}") + @Put("/{subFolderName}") @Tags("SubFolder") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) @@ -119,7 +119,7 @@ export class SubFolderController extends Controller { }); } - @Delete("/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}") + @Delete("/{subFolderName}") @Tags("SubFolder") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) From 6718b4a10b4c63030e694fba485285d0930c0df7 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:57:48 +0700 Subject: [PATCH 27/39] docs: added description --- Prototype/server/src/utils/minio.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Prototype/server/src/utils/minio.ts b/Prototype/server/src/utils/minio.ts index ed39987..b9c49b4 100644 --- a/Prototype/server/src/utils/minio.ts +++ b/Prototype/server/src/utils/minio.ts @@ -3,11 +3,26 @@ import minioClient from "../storage"; /** * Remove slash at the start and ensure slash at the end of the path + * @param path - path to be check and ensure + * @returns path without / at start and end with trailing slash */ function safePath(path: string) { return path.replace(/^\/|\/$/g, "") + "/"; } +/** + * Replace illegal character eg. ? % < > / \ : | that can't be in path with "-". + * @param path - string to check and replace + * @returns path with illegal character replaced with "-" + */ +export function replaceIllegalChars(path: string, replaceChar = "-") { + return path.replace(/[/\\?%*:|"<>]/g, "-"); +} + +/** + * Utility function to check for .keep file if it is exist or not. + * @returns true if .keep exist, false otherwise + */ export async function pathExist(path: string): Promise { return await minioClient .statObject("ehr", `${safePath(path)}.keep`) @@ -18,6 +33,11 @@ export async function pathExist(path: string): Promise { }); } +/** + * Utility function to list folder by using .keep file with prefix in minio. + * @param path - path to list + * @return list of folder with metadata + */ export function listFolder(path?: string): Promise { if (path) path = safePath(path); From d3a32e2f8a961eb51ce16d6e1bba725881d75847 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 21 Nov 2023 10:01:57 +0700 Subject: [PATCH 28/39] refactor: prevent user from create folder with illegal chars --- .../src/controllers/cabinetController.ts | 8 ++++--- .../src/controllers/drawerController.ts | 8 ++++--- .../src/controllers/folderController.ts | 22 ++++++++++++------- .../src/controllers/subFolderController.ts | 22 ++++++++++++------- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/Prototype/server/src/controllers/cabinetController.ts b/Prototype/server/src/controllers/cabinetController.ts index 7e64364..e7e7102 100644 --- a/Prototype/server/src/controllers/cabinetController.ts +++ b/Prototype/server/src/controllers/cabinetController.ts @@ -17,7 +17,7 @@ import minioClient from "../storage"; import { EhrFolder } from "../interfaces/ehr-fs"; import HttpStatusCode from "../interfaces/http-status"; -import { listFolder } from "../utils/minio"; +import { listFolder, replaceIllegalChars } from "../utils/minio"; @Route("cabinet") export class CabinetController extends Controller { @@ -37,7 +37,7 @@ export class CabinetController extends Controller { @Body() body: { name: string }, ) { const uploaded = await minioClient - .putObject("ehr", `${body.name}/.keep`, "", 0, { + .putObject("ehr", `${replaceIllegalChars(body.name)}/.keep`, "", 0, { createdAt: new Date().toISOString(), createdBy: request.user.preferred_username, }) @@ -62,7 +62,9 @@ export class CabinetController extends Controller { stream.on("data", (v) => { if (!(v && v.name)) return; - const destination = `${body.name}/${v.name.slice(cabinetName.length + 1)}`; + const destination = `${replaceIllegalChars(body.name)}/${v.name.slice( + cabinetName.length + 1, + )}`; const source = `/ehr/${v.name}`; const cond = new Minio.CopyConditions(); diff --git a/Prototype/server/src/controllers/drawerController.ts b/Prototype/server/src/controllers/drawerController.ts index 4bf9002..6fe5d21 100644 --- a/Prototype/server/src/controllers/drawerController.ts +++ b/Prototype/server/src/controllers/drawerController.ts @@ -17,7 +17,7 @@ import minioClient from "../storage"; import HttpStatusCode from "../interfaces/http-status"; import HttpError from "../interfaces/http-error"; -import { listFolder, pathExist } from "../utils/minio"; +import { listFolder, pathExist, replaceIllegalChars } from "../utils/minio"; @Route("/cabinet/{cabinetName}/drawer") export class DrawerController extends Controller { @@ -42,7 +42,7 @@ export class DrawerController extends Controller { } const uploaded = await minioClient - .putObject("ehr", `${cabinetName}/${body.name}/.keep`, "", 0, { + .putObject("ehr", `${cabinetName}/${replaceIllegalChars(body.name)}/.keep`, "", 0, { createdAt: new Date().toISOString(), createdBy: request.user.preferred_username, }) @@ -76,7 +76,9 @@ export class DrawerController extends Controller { stream.on("data", (v) => { if (!(v && v.name)) return; - const destination = `${cabinetName}/${body.name}/${v.name.slice(fullpath.length)}`; + const destination = `${cabinetName}/${replaceIllegalChars(body.name)}/${v.name.slice( + fullpath.length, + )}`; const source = `/ehr/${v.name}`; const cond = new Minio.CopyConditions(); diff --git a/Prototype/server/src/controllers/folderController.ts b/Prototype/server/src/controllers/folderController.ts index d7e4a47..5c7c35d 100644 --- a/Prototype/server/src/controllers/folderController.ts +++ b/Prototype/server/src/controllers/folderController.ts @@ -17,7 +17,7 @@ import * as Minio from "minio"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; -import { listFolder, pathExist } from "../utils/minio"; +import { listFolder, pathExist, replaceIllegalChars } from "../utils/minio"; import { EhrFolder } from "../interfaces/ehr-fs"; import minioClient from "../storage"; @@ -54,10 +54,16 @@ export class FolderController extends Controller { } const uploaded = await minioClient - .putObject("ehr", `${cabinetName}/${drawerName}/${body.name}/.keep`, "", 0, { - createdAt: new Date().toISOString(), - createdBy: request.user.preferred_username, - }) + .putObject( + "ehr", + `${cabinetName}/${drawerName}/${replaceIllegalChars(body.name)}/.keep`, + "", + 0, + { + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + }, + ) .catch((e) => console.error(e)); if (!uploaded) { @@ -92,9 +98,9 @@ export class FolderController extends Controller { stream.on("data", (v) => { if (!(v && v.name)) return; - const destination = `${cabinetName}/${drawerName}/${body.name}/${v.name.slice( - fullpath.length, - )}`; + const destination = `${cabinetName}/${drawerName}/${replaceIllegalChars( + body.name, + )}/${v.name.slice(fullpath.length)}`; const source = `/ehr/${v.name}`; const cond = new Minio.CopyConditions(); diff --git a/Prototype/server/src/controllers/subFolderController.ts b/Prototype/server/src/controllers/subFolderController.ts index f72a2d4..abf6192 100644 --- a/Prototype/server/src/controllers/subFolderController.ts +++ b/Prototype/server/src/controllers/subFolderController.ts @@ -17,7 +17,7 @@ import * as Minio from "minio"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; -import { listFolder, pathExist } from "../utils/minio"; +import { listFolder, pathExist, replaceIllegalChars } from "../utils/minio"; import { EhrFolder } from "../interfaces/ehr-fs"; import minioClient from "../storage"; @@ -59,10 +59,16 @@ export class SubFolderController extends Controller { } const uploaded = await minioClient - .putObject("ehr", `${cabinetName}/${drawerName}/${folderName}/${body.name}/.keep`, "", 0, { - createdAt: new Date().toISOString(), - createdBy: request.user.preferred_username, - }) + .putObject( + "ehr", + `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(body.name)}/.keep`, + "", + 0, + { + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + }, + ) .catch((e) => console.error(e)); if (!uploaded) { @@ -98,9 +104,9 @@ export class SubFolderController extends Controller { stream.on("data", (v) => { if (!(v && v.name)) return; - const destination = `${cabinetName}/${drawerName}/${folderName}/${body.name}/${v.name.slice( - fullpath.length, - )}`; + const destination = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars( + body.name, + )}/${v.name.slice(fullpath.length)}`; const source = `/ehr/${v.name}`; const cond = new Minio.CopyConditions(); From 914dcc5e2c0d220b53a1c156cd6a9cc9823c5e79 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 21 Nov 2023 10:13:27 +0700 Subject: [PATCH 29/39] fix: unused char --- Prototype/server/src/utils/minio.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prototype/server/src/utils/minio.ts b/Prototype/server/src/utils/minio.ts index b9c49b4..131ee1a 100644 --- a/Prototype/server/src/utils/minio.ts +++ b/Prototype/server/src/utils/minio.ts @@ -16,7 +16,7 @@ function safePath(path: string) { * @returns path with illegal character replaced with "-" */ export function replaceIllegalChars(path: string, replaceChar = "-") { - return path.replace(/[/\\?%*:|"<>]/g, "-"); + return path.replace(/[/\\?%*:|"<>]/g, replaceChar); } /** From 3ff307720bb6138659684bf705b6999bb2f1e2bd Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 21 Nov 2023 11:46:40 +0700 Subject: [PATCH 30/39] feat: subfolder file crud --- .../controllers/subFolderFileController.ts | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 Prototype/server/src/controllers/subFolderFileController.ts diff --git a/Prototype/server/src/controllers/subFolderFileController.ts b/Prototype/server/src/controllers/subFolderFileController.ts new file mode 100644 index 0000000..93605ea --- /dev/null +++ b/Prototype/server/src/controllers/subFolderFileController.ts @@ -0,0 +1,290 @@ +import { + Controller, + Delete, + FormField, + Get, + Patch, + Path, + Post, + Request, + Route, + Security, + SuccessResponse, + Tags, + UploadedFile, +} from "tsoa"; +import esClient from "../elasticsearch"; +import minioClient from "../storage"; +import HttpStatusCode from "../interfaces/http-status"; +import { pathExist } from "../utils/minio"; +import HttpError from "../interfaces/http-error"; +import { EhrFile } from "../interfaces/ehr-fs"; + +@Route( + "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}/file", +) +export class SubFolderFileController extends Controller { + @Post("/") + @Tags("SubFolder File") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.CREATED) + public async uploadFile( + @Request() request: { user: { preferred_username: string } }, + @UploadedFile() file: Express.Multer.File, + @FormField() title: string, + @FormField() description: string, + @FormField() keyword: string, + @FormField() category: string, + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() subFolderName: string, + ) { + const filename = Buffer.from(file.originalname, "latin1").toString("utf-8"); + const pathname = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${filename}`; + + if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/${subFolderName}`))) { + throw new HttpError( + HttpStatusCode.PRECONDITION_FAILED, + "Cabinet, drawer, folder or subfolder cannot be found.", + ); + } + + const info = await minioClient + .putObject("ehr", pathname, file.buffer, file.size, { + "Content-Type": file.mimetype, + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + }) + .catch((e) => console.error(e)); + + if (!info) throw new Error("Object storage error occured."); + + const search = await esClient.search }>({ + index: "ehr-api-client", + query: { + match: { + pathname: pathname, + }, + }, + }); + + const exist = search.hits.hits.find((v) => v._source?.pathname === pathname); + + const metadata: Partial = { + pathname, + fileName: filename, + fileSize: file.size, + fileType: file.mimetype, + title: title, + description: description, + category: category.split(","), + keyword: keyword.split(","), + }; + + if (!exist) { + await esClient.index({ + pipeline: "attachment", + index: "ehr-api-client", + document: { + data: Buffer.from(file.buffer).toString("base64"), + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username, + ...metadata, + }, + }); + } else { + await esClient.delete({ index: exist._index, id: exist._id }); + await esClient.index({ + pipeline: "attachment", + index: "ehr-api-client", + document: { + data: Buffer.from(file.buffer).toString("base64"), + createdAt: exist._source?.createdAt, + createdBy: exist._source?.createdBy, + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username, + ...metadata, + }, + }); + } + + return this.setStatus(HttpStatusCode.CREATED); + } + + @Get("/") + @Tags("SubFolder File") + @SuccessResponse(HttpStatusCode.OK) + public async getFile( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() subFolderName: string, + ) { + const search = await esClient.search< + EhrFile & { + attachment: Record; + } + >({ + index: "ehr-api-client", + query: { + prefix: { + pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, + }, + }, + }); + + // Use flatMap for return type only. Filter does not change type after filter out undefined or null + const records = search.hits.hits + .map((v) => { + if (!v._source) return; + + const { attachment, ...rest } = v._source; + + return rest; + }) + .flatMap((v) => (v ? [v] : [])); + + return records; + } + + @Patch("/{fileName}") + @Tags("SubFolder File") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK) + public async updateFile( + @Request() request: { user: { preferred_username: string } }, + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() subFolderName: string, + @Path() fileName: string, + @UploadedFile() file?: Express.Multer.File, + @FormField() title?: string, + @FormField() description?: string, + @FormField() keyword?: string, + @FormField() category?: string, + ) { + const search = await esClient.search }>({ + index: "ehr-api-client", + query: { + match: { + pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`, + }, + }, + }); + + if (search && search.hits.hits.length === 0) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); + } + + const data = search.hits.hits[0]; + + if (!file) { + const esResult = await esClient + .update({ + index: "ehr-api-client", + id: data._id, + doc: { + title, + description, + keyword: keyword?.split(","), + category: category?.split(","), + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username, + }, + }) + .catch((e) => console.error(e)); + + if (!esResult) throw new Error("An error occured, cannot perform this action."); + } else { + const filename = Buffer.from(file.originalname, "latin1").toString("utf-8"); + const pathname = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${filename}`; + + await minioClient.removeObject( + "ehr", + `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`, + ); + + const info = await minioClient + .putObject("ehr", pathname, file.buffer, file.size, { + "Content-Type": file.mimetype, + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + }) + .catch((e) => console.error(e)); + + if (!info) throw new Error("Object storage error occured."); + + await esClient.delete({ index: data._index, id: data._id }); + await esClient.index({ + pipeline: "attachment", + index: "ehr-api-client", + document: { + data: Buffer.from(file.buffer).toString("base64"), + pathname, + fileName: filename, + fileSize: file.size, + fileType: file.mimetype, + title: title, + description: description, + category: category?.split(","), + keyword: keyword?.split(","), + createdAt: data._source?.createdAt, + createdBy: data._source?.createdBy, + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username, + }, + }); + } + + return this.setStatus(HttpStatusCode.NO_CONTENT); + } + + @Delete("/{fileName}") + @Tags("SubFolder File") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK) + public async deleteFile( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() subFolderName: string, + @Path() fileName: string, + ) { + const search = await esClient.search< + EhrFile & { + attachment: Record; + } + >({ + index: "ehr-api-client", + query: { + match: { + pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`, + }, + }, + }); + + if (search && search.hits.hits.length === 0) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); + } + + const esResult = await esClient + .delete({ + index: "ehr-api-client", + id: search.hits.hits[0]._id, + }) + .catch((e) => console.error(e)); + + if (!esResult) throw new Error("An error occured, cannot perform this action."); + + await minioClient.removeObject( + "ehr", + `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`, + ); + + return this.setStatus(HttpStatusCode.NO_CONTENT); + } +} From 3247af3cab1b6259d4ceefd729f05dac6eb75b96 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 21 Nov 2023 11:47:10 +0700 Subject: [PATCH 31/39] chore: update route and swagger --- Prototype/server/src/routes.ts | 10 +++++ Prototype/server/src/swagger.json | 61 ++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 807727b..a028c3e 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -56,6 +56,15 @@ const models: TsoaRoute.Models = { "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 + "Search": { + "dataType": "refObject", + "properties": { + "AND": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"value":{"dataType":"string","required":true},"field":{"dataType":"string","required":true}}}}, + "OR": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"value":{"dataType":"string","required":true},"field":{"dataType":"string","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 validationService = new ValidationService(models); @@ -523,6 +532,7 @@ export function RegisterRoutes(app: Router) { function SearchController_searchFile(request: any, response: any, next: any) { const args = { + search: {"in":"body","name":"search","required":true,"ref":"Search"}, }; // 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/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index ed1bf8f..7778788 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -124,6 +124,48 @@ ], "type": "object", "additionalProperties": false + }, + "Search": { + "properties": { + "AND": { + "items": { + "properties": { + "value": { + "type": "string" + }, + "field": { + "type": "string" + } + }, + "required": [ + "value", + "field" + ], + "type": "object" + }, + "type": "array" + }, + "OR": { + "items": { + "properties": { + "value": { + "type": "string" + }, + "field": { + "type": "string" + } + }, + "required": [ + "value", + "field" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object", + "additionalProperties": false } }, "securitySchemes": { @@ -928,15 +970,30 @@ "content": { "application/json": { "schema": { - "items": {}, + "items": { + "$ref": "#/components/schemas/EhrFile" + }, "type": "array" } } } } }, + "tags": [ + "Search" + ], "security": [], - "parameters": [] + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Search" + } + } + } + } } }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder": { From f5277a36f8da3bce511881e25ca763b382dc601f Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:24:01 +0700 Subject: [PATCH 32/39] feat: search file --- .../src/controllers/searchController.ts | 35 +++++++++++++++++++ Prototype/server/src/interfaces/search.ts | 10 ++++++ 2 files changed, 45 insertions(+) create mode 100644 Prototype/server/src/controllers/searchController.ts create mode 100644 Prototype/server/src/interfaces/search.ts diff --git a/Prototype/server/src/controllers/searchController.ts b/Prototype/server/src/controllers/searchController.ts new file mode 100644 index 0000000..08a7b25 --- /dev/null +++ b/Prototype/server/src/controllers/searchController.ts @@ -0,0 +1,35 @@ +import { Body, Controller, Post, Route, SuccessResponse, Tags } from "tsoa"; +import HttpStatusCode from "../interfaces/http-status"; +import esClient from "../elasticsearch"; +import { Search } from "../interfaces/search"; +import { EhrFile } from "../interfaces/ehr-fs"; + +@Route("/search") +export class SearchController extends Controller { + @Post("/") + @Tags("Search") + @SuccessResponse(HttpStatusCode.OK) + public async searchFile(@Body() search: Search): Promise { + const result = await esClient.search }>({ + index: "ehr-api-client", + query: { + bool: { + must: search.AND?.map((v) => ({ match: { [v.field]: v.value } })), + should: search.OR?.map((v) => ({ match: { [v.field]: v.value } })), + }, + }, + }); + + return result.hits.hits.length > 0 + ? result.hits.hits + .map((v) => { + if (!v._source) return; + + const { attachment, ...rest } = v._source; + + return rest; + }) + .flatMap((v) => (!!v ? [v] : [])) + : []; + } +} diff --git a/Prototype/server/src/interfaces/search.ts b/Prototype/server/src/interfaces/search.ts new file mode 100644 index 0000000..74b5286 --- /dev/null +++ b/Prototype/server/src/interfaces/search.ts @@ -0,0 +1,10 @@ +export interface Search { + AND?: { + field: string; + value: string; + }[]; + OR?: { + field: string; + value: string; + }[]; +} From 4e3366654dd791c1e61803a9185441ec838b42f9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:58:45 +0700 Subject: [PATCH 33/39] feat: file download --- .../server/src/controllers/fileController.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Prototype/server/src/controllers/fileController.ts b/Prototype/server/src/controllers/fileController.ts index fe9d1f9..8c5fb09 100644 --- a/Prototype/server/src/controllers/fileController.ts +++ b/Prototype/server/src/controllers/fileController.ts @@ -278,4 +278,43 @@ export class FileController extends Controller { return this.setStatus(HttpStatusCode.NO_CONTENT); } + + @Get("/{fileName}") + @Tags("File") + @SuccessResponse(HttpStatusCode.OK) + public async downloadFile( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() fileName: string, + ) { + const search = await esClient.search }>({ + index: "ehr-api-client", + query: { + match: { + pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, + }, + }, + }); + + if (search && search.hits.hits.length === 0) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); + } + + const data = search.hits.hits[0]._source; + + if (!data) { + throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "Found data but no info."); + } + + const { attachment, ...rest } = data; + + return { + ...rest, + download: await minioClient.presignedGetObject( + "ehr", + `${cabinetName}/${drawerName}/${folderName}/${fileName}`, + ), + }; + } } From 2f36e325afe073669741242b229d4a472a50aeb8 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:59:58 +0700 Subject: [PATCH 34/39] chore: update env config --- Prototype/server/.env.example | 5 ++++- Prototype/server/src/storage/index.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Prototype/server/.env.example b/Prototype/server/.env.example index f2f6554..f2eb5d1 100644 --- a/Prototype/server/.env.example +++ b/Prototype/server/.env.example @@ -4,11 +4,14 @@ REALM_URL= PORT= MINIO_HOST=localhost -MINIO_PORT=9000 +MINIO_PORT=443 +MINIO_SSL=true MINIO_ACCESS_KEY= MINIO_SECRET_KEY= +MINIO_BUCKET= ELASTICSEARCH_PROTOCOL=http ELASTICSEARCH_HOST=localhost ELASTICSEARCH_PORT=9200 +ELASTICSEARCH_INDEX= diff --git a/Prototype/server/src/storage/index.ts b/Prototype/server/src/storage/index.ts index ca2a279..48db781 100644 --- a/Prototype/server/src/storage/index.ts +++ b/Prototype/server/src/storage/index.ts @@ -2,8 +2,8 @@ import * as Minio from "minio"; const minioClient = new Minio.Client({ endPoint: process.env.MINIO_HOST ?? "localhost", - port: +(process.env.MINIO_PORT || 9000), - useSSL: false, + port: process.env.MINIO_PORT ? +process.env.MINIO_PORT : undefined, + useSSL: !!process.env.MINIO_SSL, accessKey: process.env.MINIO_ACCESS_KEY ?? "", secretKey: process.env.MINIO_SECRET_KEY ?? "", }); From 1e2b26558c385dc4e0367b68f1c19172d57fd1bd Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:45:56 +0700 Subject: [PATCH 35/39] feat: subfolder file download --- .../controllers/subFolderFileController.ts | 40 +++ Prototype/server/src/routes.ts | 84 ++++++ Prototype/server/src/swagger.json | 284 ++++++++++++++++++ 3 files changed, 408 insertions(+) diff --git a/Prototype/server/src/controllers/subFolderFileController.ts b/Prototype/server/src/controllers/subFolderFileController.ts index 93605ea..4162877 100644 --- a/Prototype/server/src/controllers/subFolderFileController.ts +++ b/Prototype/server/src/controllers/subFolderFileController.ts @@ -287,4 +287,44 @@ export class SubFolderFileController extends Controller { return this.setStatus(HttpStatusCode.NO_CONTENT); } + + @Get("/{fileName}") + @Tags("File") + @SuccessResponse(HttpStatusCode.OK) + public async downloadFile( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() subFolderName: string, + @Path() fileName: string, + ) { + const search = await esClient.search }>({ + index: "ehr-api-client", + query: { + match: { + pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`, + }, + }, + }); + + if (search && search.hits.hits.length === 0) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); + } + + const data = search.hits.hits[0]._source; + + if (!data) { + throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "Found data but no info."); + } + + const { attachment, ...rest } = data; + + return { + ...rest, + download: await minioClient.presignedGetObject( + "ehr", + `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`, + ), + }; + } } diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index a028c3e..3068d7a 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -11,6 +11,8 @@ import { FileController } from './controllers/fileController'; // 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 { FolderController } from './controllers/folderController'; // 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 { MinioController } from './controllers/minioController'; +// 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 { SearchController } from './controllers/searchController'; // 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 { SubFolderController } from './controllers/subFolderController'; @@ -414,6 +416,34 @@ export function RegisterRoutes(app: Router) { } }); // 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName', + ...(fetchMiddlewares(FileController)), + ...(fetchMiddlewares(FileController.prototype.downloadFile)), + + function FileController_downloadFile(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new FileController(); + + + const promise = controller.downloadFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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('/cabinet/:cabinetName/drawer/:drawerName/folder', ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.listFolder)), @@ -526,6 +556,31 @@ export function RegisterRoutes(app: Router) { } }); // 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.post('/minio', + ...(fetchMiddlewares(MinioController)), + ...(fetchMiddlewares(MinioController.prototype.hook)), + + function MinioController_hook(request: any, response: any, next: any) { + const args = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new MinioController(); + + + const promise = controller.hook.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, next); + } 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.post('/search', ...(fetchMiddlewares(SearchController)), ...(fetchMiddlewares(SearchController.prototype.searchFile)), @@ -798,6 +853,35 @@ export function RegisterRoutes(app: Router) { } }); // 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file/:fileName', + ...(fetchMiddlewares(SubFolderFileController)), + ...(fetchMiddlewares(SubFolderFileController.prototype.downloadFile)), + + function SubFolderFileController_downloadFile(request: any, response: any, next: any) { + const args = { + cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, + drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, + folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, + subFolderName: {"in":"path","name":"subFolderName","required":true,"dataType":"string"}, + fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"}, + }; + + // 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 = getValidatedArgs(args, request, response); + + const controller = new SubFolderFileController(); + + + const promise = controller.downloadFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 200, next); + } 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 diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index 7778788..1128fbb 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -751,6 +751,138 @@ } } ] + }, + "get": { + "operationId": "DownloadFile", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "properties": { + "createdBy": { + "type": "string" + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "updatedBy": { + "type": "string" + }, + "updatedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "keyword": { + "items": { + "type": "string" + }, + "type": "array" + }, + "category": { + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "fileType": { + "type": "string" + }, + "fileSize": { + "type": "number", + "format": "double" + }, + "fileName": { + "type": "string" + }, + "pathname": { + "type": "string" + }, + "download": { + "type": "string" + } + }, + "required": [ + "createdBy", + "createdAt", + "updatedBy", + "updatedAt", + "keyword", + "category", + "description", + "title", + "fileType", + "fileSize", + "fileName", + "pathname", + "download" + ], + "type": "object" + } + } + } + } + }, + "tags": [ + "File" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "fileName", + "required": true, + "schema": { + "type": "string" + } + } + ] } }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder": { @@ -961,6 +1093,18 @@ ] } }, + "/minio": { + "post": { + "operationId": "Hook", + "responses": { + "204": { + "description": "No content" + } + }, + "security": [], + "parameters": [] + } + }, "/search": { "post": { "operationId": "SearchFile", @@ -1598,6 +1742,146 @@ } } ] + }, + "get": { + "operationId": "DownloadFile", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "properties": { + "createdBy": { + "type": "string" + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "updatedBy": { + "type": "string" + }, + "updatedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "keyword": { + "items": { + "type": "string" + }, + "type": "array" + }, + "category": { + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "fileType": { + "type": "string" + }, + "fileSize": { + "type": "number", + "format": "double" + }, + "fileName": { + "type": "string" + }, + "pathname": { + "type": "string" + }, + "download": { + "type": "string" + } + }, + "required": [ + "createdBy", + "createdAt", + "updatedBy", + "updatedAt", + "keyword", + "category", + "description", + "title", + "fileType", + "fileSize", + "fileName", + "pathname", + "download" + ], + "type": "object" + } + } + } + } + }, + "tags": [ + "File" + ], + "security": [], + "parameters": [ + { + "in": "path", + "name": "cabinetName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "drawerName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "folderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "subFolderName", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "fileName", + "required": true, + "schema": { + "type": "string" + } + } + ] } } }, From 2405eac9e10be02411784b4f2fd74f9f4857c4f8 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:09:09 +0700 Subject: [PATCH 36/39] chore: build process --- Prototype/server/.dockerignore | 6 ++++++ Prototype/server/Dockerfile | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 Prototype/server/.dockerignore create mode 100644 Prototype/server/Dockerfile diff --git a/Prototype/server/.dockerignore b/Prototype/server/.dockerignore new file mode 100644 index 0000000..b740314 --- /dev/null +++ b/Prototype/server/.dockerignore @@ -0,0 +1,6 @@ +.DS_Store +node_modules +/dist +.env +.env.* +!.env.example diff --git a/Prototype/server/Dockerfile b/Prototype/server/Dockerfile new file mode 100644 index 0000000..1406416 --- /dev/null +++ b/Prototype/server/Dockerfile @@ -0,0 +1,24 @@ +FROM node:18-alpine as base + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN corepack enable + +WORKDIR /app + +COPY . . + +FROM base AS deps +RUN npm install + +FROM base AS build +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +RUN pnpm run build + +FROM base as prod +COPY --from=deps /app/node_modules /app/node_modules +COPY --from=build /app/dist /app/dist +COPY --from=base /app/static /app/static + +CMD ["node", "./dist/app.js"] From e58e4db933acbb9573a5257e7392d8ec48c75ef3 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:11:21 +0700 Subject: [PATCH 37/39] chore: generate route and swagger --- Prototype/server/src/routes.ts | 27 --------------------------- Prototype/server/src/swagger.json | 12 ------------ 2 files changed, 39 deletions(-) diff --git a/Prototype/server/src/routes.ts b/Prototype/server/src/routes.ts index 3068d7a..d16e76d 100644 --- a/Prototype/server/src/routes.ts +++ b/Prototype/server/src/routes.ts @@ -11,8 +11,6 @@ import { FileController } from './controllers/fileController'; // 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 { FolderController } from './controllers/folderController'; // 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 { MinioController } from './controllers/minioController'; -// 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 { SearchController } from './controllers/searchController'; // 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 { SubFolderController } from './controllers/subFolderController'; @@ -556,31 +554,6 @@ export function RegisterRoutes(app: Router) { } }); // 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.post('/minio', - ...(fetchMiddlewares(MinioController)), - ...(fetchMiddlewares(MinioController.prototype.hook)), - - function MinioController_hook(request: any, response: any, next: any) { - const args = { - req: {"in":"request","name":"req","required":true,"dataType":"object"}, - }; - - // 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 = getValidatedArgs(args, request, response); - - const controller = new MinioController(); - - - const promise = controller.hook.apply(controller, validatedArgs as any); - promiseHandler(controller, promise, response, undefined, next); - } 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.post('/search', ...(fetchMiddlewares(SearchController)), ...(fetchMiddlewares(SearchController.prototype.searchFile)), diff --git a/Prototype/server/src/swagger.json b/Prototype/server/src/swagger.json index 1128fbb..69080a3 100644 --- a/Prototype/server/src/swagger.json +++ b/Prototype/server/src/swagger.json @@ -1093,18 +1093,6 @@ ] } }, - "/minio": { - "post": { - "operationId": "Hook", - "responses": { - "204": { - "description": "No content" - } - }, - "security": [], - "parameters": [] - } - }, "/search": { "post": { "operationId": "SearchFile", From f3078c47ea6d71bf8ad29849f191373cd9a28a2a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:12:03 +0700 Subject: [PATCH 38/39] chore: server listen to 0.0.0.0 --- Prototype/server/src/app.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Prototype/server/src/app.ts b/Prototype/server/src/app.ts index ac593ed..efefe83 100644 --- a/Prototype/server/src/app.ts +++ b/Prototype/server/src/app.ts @@ -25,4 +25,6 @@ RegisterRoutes(router); app.use(swaggerSpecs.basePath, router); app.use(errorHandler); -app.listen(PORT, () => console.log(`Application is running on http://localhost:${PORT}`)); +app.listen(PORT, "0.0.0.0", () => + console.log(`Application is running on http://localhost:${PORT}`), +); From 7c55806956e89d0ead747d8ba6660613c90bfce6 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:16:27 +0700 Subject: [PATCH 39/39] chore: change dir (prepare to merge) --- {Prototype => Services}/server/.dockerignore | 0 {Prototype => Services}/server/.env.example | 0 {Prototype => Services}/server/.gitignore | 0 {Prototype => Services}/server/.prettierignore | 0 {Prototype => Services}/server/.prettierrc | 0 {Prototype => Services}/server/Dockerfile | 0 {Prototype => Services}/server/nodemon.json | 0 {Prototype => Services}/server/package.json | 0 {Prototype => Services}/server/pnpm-lock.yaml | 0 {Prototype => Services}/server/src/app.ts | 0 .../server/src/controllers/cabinetController.ts | 0 .../server/src/controllers/drawerController.ts | 0 {Prototype => Services}/server/src/controllers/fileController.ts | 0 .../server/src/controllers/folderController.ts | 0 .../server/src/controllers/searchController.ts | 0 .../server/src/controllers/subFolderController.ts | 0 .../server/src/controllers/subFolderFileController.ts | 0 {Prototype => Services}/server/src/elasticsearch/index.ts | 0 {Prototype => Services}/server/src/interfaces/ehr-fs.ts | 0 {Prototype => Services}/server/src/interfaces/http-error.ts | 0 {Prototype => Services}/server/src/interfaces/http-status.ts | 0 {Prototype => Services}/server/src/interfaces/search.ts | 0 {Prototype => Services}/server/src/middlewares/exception.ts | 0 {Prototype => Services}/server/src/routes.ts | 0 {Prototype => Services}/server/src/storage/index.ts | 0 {Prototype => Services}/server/src/swagger.json | 0 {Prototype => Services}/server/src/utils/auth.ts | 0 {Prototype => Services}/server/src/utils/minio.ts | 0 {Prototype => Services}/server/static/.gitkeep | 0 {Prototype => Services}/server/tsconfig.json | 0 {Prototype => Services}/server/tsoa.json | 0 31 files changed, 0 insertions(+), 0 deletions(-) rename {Prototype => Services}/server/.dockerignore (100%) rename {Prototype => Services}/server/.env.example (100%) rename {Prototype => Services}/server/.gitignore (100%) rename {Prototype => Services}/server/.prettierignore (100%) rename {Prototype => Services}/server/.prettierrc (100%) rename {Prototype => Services}/server/Dockerfile (100%) rename {Prototype => Services}/server/nodemon.json (100%) rename {Prototype => Services}/server/package.json (100%) rename {Prototype => Services}/server/pnpm-lock.yaml (100%) rename {Prototype => Services}/server/src/app.ts (100%) rename {Prototype => Services}/server/src/controllers/cabinetController.ts (100%) rename {Prototype => Services}/server/src/controllers/drawerController.ts (100%) rename {Prototype => Services}/server/src/controllers/fileController.ts (100%) rename {Prototype => Services}/server/src/controllers/folderController.ts (100%) rename {Prototype => Services}/server/src/controllers/searchController.ts (100%) rename {Prototype => Services}/server/src/controllers/subFolderController.ts (100%) rename {Prototype => Services}/server/src/controllers/subFolderFileController.ts (100%) rename {Prototype => Services}/server/src/elasticsearch/index.ts (100%) rename {Prototype => Services}/server/src/interfaces/ehr-fs.ts (100%) rename {Prototype => Services}/server/src/interfaces/http-error.ts (100%) rename {Prototype => Services}/server/src/interfaces/http-status.ts (100%) rename {Prototype => Services}/server/src/interfaces/search.ts (100%) rename {Prototype => Services}/server/src/middlewares/exception.ts (100%) rename {Prototype => Services}/server/src/routes.ts (100%) rename {Prototype => Services}/server/src/storage/index.ts (100%) rename {Prototype => Services}/server/src/swagger.json (100%) rename {Prototype => Services}/server/src/utils/auth.ts (100%) rename {Prototype => Services}/server/src/utils/minio.ts (100%) rename {Prototype => Services}/server/static/.gitkeep (100%) rename {Prototype => Services}/server/tsconfig.json (100%) rename {Prototype => Services}/server/tsoa.json (100%) diff --git a/Prototype/server/.dockerignore b/Services/server/.dockerignore similarity index 100% rename from Prototype/server/.dockerignore rename to Services/server/.dockerignore diff --git a/Prototype/server/.env.example b/Services/server/.env.example similarity index 100% rename from Prototype/server/.env.example rename to Services/server/.env.example diff --git a/Prototype/server/.gitignore b/Services/server/.gitignore similarity index 100% rename from Prototype/server/.gitignore rename to Services/server/.gitignore diff --git a/Prototype/server/.prettierignore b/Services/server/.prettierignore similarity index 100% rename from Prototype/server/.prettierignore rename to Services/server/.prettierignore diff --git a/Prototype/server/.prettierrc b/Services/server/.prettierrc similarity index 100% rename from Prototype/server/.prettierrc rename to Services/server/.prettierrc diff --git a/Prototype/server/Dockerfile b/Services/server/Dockerfile similarity index 100% rename from Prototype/server/Dockerfile rename to Services/server/Dockerfile diff --git a/Prototype/server/nodemon.json b/Services/server/nodemon.json similarity index 100% rename from Prototype/server/nodemon.json rename to Services/server/nodemon.json diff --git a/Prototype/server/package.json b/Services/server/package.json similarity index 100% rename from Prototype/server/package.json rename to Services/server/package.json diff --git a/Prototype/server/pnpm-lock.yaml b/Services/server/pnpm-lock.yaml similarity index 100% rename from Prototype/server/pnpm-lock.yaml rename to Services/server/pnpm-lock.yaml diff --git a/Prototype/server/src/app.ts b/Services/server/src/app.ts similarity index 100% rename from Prototype/server/src/app.ts rename to Services/server/src/app.ts diff --git a/Prototype/server/src/controllers/cabinetController.ts b/Services/server/src/controllers/cabinetController.ts similarity index 100% rename from Prototype/server/src/controllers/cabinetController.ts rename to Services/server/src/controllers/cabinetController.ts diff --git a/Prototype/server/src/controllers/drawerController.ts b/Services/server/src/controllers/drawerController.ts similarity index 100% rename from Prototype/server/src/controllers/drawerController.ts rename to Services/server/src/controllers/drawerController.ts diff --git a/Prototype/server/src/controllers/fileController.ts b/Services/server/src/controllers/fileController.ts similarity index 100% rename from Prototype/server/src/controllers/fileController.ts rename to Services/server/src/controllers/fileController.ts diff --git a/Prototype/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts similarity index 100% rename from Prototype/server/src/controllers/folderController.ts rename to Services/server/src/controllers/folderController.ts diff --git a/Prototype/server/src/controllers/searchController.ts b/Services/server/src/controllers/searchController.ts similarity index 100% rename from Prototype/server/src/controllers/searchController.ts rename to Services/server/src/controllers/searchController.ts diff --git a/Prototype/server/src/controllers/subFolderController.ts b/Services/server/src/controllers/subFolderController.ts similarity index 100% rename from Prototype/server/src/controllers/subFolderController.ts rename to Services/server/src/controllers/subFolderController.ts diff --git a/Prototype/server/src/controllers/subFolderFileController.ts b/Services/server/src/controllers/subFolderFileController.ts similarity index 100% rename from Prototype/server/src/controllers/subFolderFileController.ts rename to Services/server/src/controllers/subFolderFileController.ts diff --git a/Prototype/server/src/elasticsearch/index.ts b/Services/server/src/elasticsearch/index.ts similarity index 100% rename from Prototype/server/src/elasticsearch/index.ts rename to Services/server/src/elasticsearch/index.ts diff --git a/Prototype/server/src/interfaces/ehr-fs.ts b/Services/server/src/interfaces/ehr-fs.ts similarity index 100% rename from Prototype/server/src/interfaces/ehr-fs.ts rename to Services/server/src/interfaces/ehr-fs.ts diff --git a/Prototype/server/src/interfaces/http-error.ts b/Services/server/src/interfaces/http-error.ts similarity index 100% rename from Prototype/server/src/interfaces/http-error.ts rename to Services/server/src/interfaces/http-error.ts diff --git a/Prototype/server/src/interfaces/http-status.ts b/Services/server/src/interfaces/http-status.ts similarity index 100% rename from Prototype/server/src/interfaces/http-status.ts rename to Services/server/src/interfaces/http-status.ts diff --git a/Prototype/server/src/interfaces/search.ts b/Services/server/src/interfaces/search.ts similarity index 100% rename from Prototype/server/src/interfaces/search.ts rename to Services/server/src/interfaces/search.ts diff --git a/Prototype/server/src/middlewares/exception.ts b/Services/server/src/middlewares/exception.ts similarity index 100% rename from Prototype/server/src/middlewares/exception.ts rename to Services/server/src/middlewares/exception.ts diff --git a/Prototype/server/src/routes.ts b/Services/server/src/routes.ts similarity index 100% rename from Prototype/server/src/routes.ts rename to Services/server/src/routes.ts diff --git a/Prototype/server/src/storage/index.ts b/Services/server/src/storage/index.ts similarity index 100% rename from Prototype/server/src/storage/index.ts rename to Services/server/src/storage/index.ts diff --git a/Prototype/server/src/swagger.json b/Services/server/src/swagger.json similarity index 100% rename from Prototype/server/src/swagger.json rename to Services/server/src/swagger.json diff --git a/Prototype/server/src/utils/auth.ts b/Services/server/src/utils/auth.ts similarity index 100% rename from Prototype/server/src/utils/auth.ts rename to Services/server/src/utils/auth.ts diff --git a/Prototype/server/src/utils/minio.ts b/Services/server/src/utils/minio.ts similarity index 100% rename from Prototype/server/src/utils/minio.ts rename to Services/server/src/utils/minio.ts diff --git a/Prototype/server/static/.gitkeep b/Services/server/static/.gitkeep similarity index 100% rename from Prototype/server/static/.gitkeep rename to Services/server/static/.gitkeep diff --git a/Prototype/server/tsconfig.json b/Services/server/tsconfig.json similarity index 100% rename from Prototype/server/tsconfig.json rename to Services/server/tsconfig.json diff --git a/Prototype/server/tsoa.json b/Services/server/tsoa.json similarity index 100% rename from Prototype/server/tsoa.json rename to Services/server/tsoa.json