From bd8290b6b19f27c6c889c99c5f6823c3ec8763c1 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 24 Nov 2023 11:32:04 +0700 Subject: [PATCH 01/29] fix: delete does not remove data from database --- .../src/controllers/cabinetController.ts | 29 ++++++++++++++--- .../src/controllers/drawerController.ts | 29 ++++++++++++++--- .../src/controllers/folderController.ts | 31 ++++++++++++++++--- .../src/controllers/subFolderController.ts | 31 ++++++++++++++++--- 4 files changed, 102 insertions(+), 18 deletions(-) diff --git a/Services/server/src/controllers/cabinetController.ts b/Services/server/src/controllers/cabinetController.ts index 655ce60..2aeab4d 100644 --- a/Services/server/src/controllers/cabinetController.ts +++ b/Services/server/src/controllers/cabinetController.ts @@ -121,7 +121,7 @@ export class CabinetController extends Controller { @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) public async deleteCabinet(@Path() cabinetName: string) { - return new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const objects: string[] = []; const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/`, true); @@ -131,10 +131,31 @@ export class CabinetController extends Controller { objects.push(v.name); }); - stream.on("close", () => minioClient.removeObjects("ehr", objects)); + stream.on("close", async () => { + minioClient.removeObjects("ehr", objects); + resolve(); + }); stream.on("error", () => reject(new Error("Object storage error occured."))); - - resolve(this.setStatus(HttpStatusCode.NO_CONTENT)); }); + + const searchResult = await esClient.search({ + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + query: { + prefix: { pathname: `${cabinetName}/` }, + }, + }); + + await Promise.all( + searchResult.hits.hits.map(async (v) => { + return esClient + .delete({ + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + id: v._id, + }) + .catch((e) => console.error(`ElasticSearch Error: ${e}`)); + }), + ); + + return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/controllers/drawerController.ts b/Services/server/src/controllers/drawerController.ts index 08b512e..e9fcf83 100644 --- a/Services/server/src/controllers/drawerController.ts +++ b/Services/server/src/controllers/drawerController.ts @@ -132,7 +132,7 @@ export class DrawerController extends Controller { @Security("bearerAuth") @SuccessResponse(HttpStatusCode.NO_CONTENT) public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) { - return new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const objects: string[] = []; const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/${drawerName}/`, true); @@ -142,10 +142,31 @@ export class DrawerController extends Controller { objects.push(v.name); }); - stream.on("close", () => minioClient.removeObjects("ehr", objects)); + stream.on("close", async () => { + minioClient.removeObjects("ehr", objects); + resolve(); + }); stream.on("error", () => reject(new Error("Object storage error occured."))); - - resolve(true); }); + + const searchResult = await esClient.search({ + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + query: { + prefix: { pathname: `${cabinetName}/${drawerName}/` }, + }, + }); + + await Promise.all( + searchResult.hits.hits.map(async (v) => { + return esClient + .delete({ + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + id: v._id, + }) + .catch((e) => console.error(`ElasticSearch Error: ${e}`)); + }), + ); + + return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index a0bb938..fe554cb 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -147,11 +147,11 @@ export class FolderController extends Controller { @Path() drawerName: string, @Path() folderName: string, ) { - return new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const objects: string[] = []; const stream = minioClient.listObjectsV2( "ehr", - `${cabinetName}/${drawerName}/${folderName}`, + `${cabinetName}/${drawerName}/${folderName}/`, true, ); @@ -161,10 +161,31 @@ export class FolderController extends Controller { objects.push(v.name); }); - stream.on("close", () => minioClient.removeObjects("ehr", objects)); + stream.on("close", async () => { + minioClient.removeObjects("ehr", objects); + resolve(); + }); stream.on("error", () => reject(new Error("Object storage error occured."))); - - resolve(this.setStatus(HttpStatusCode.NO_CONTENT)); }); + + const searchResult = await esClient.search({ + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + query: { + prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/` }, + }, + }); + + await Promise.all( + searchResult.hits.hits.map(async (v) => { + return esClient + .delete({ + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + id: v._id, + }) + .catch((e) => console.error(`ElasticSearch Error: ${e}`)); + }), + ); + + return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/controllers/subFolderController.ts b/Services/server/src/controllers/subFolderController.ts index 2ab4356..f54a79c 100644 --- a/Services/server/src/controllers/subFolderController.ts +++ b/Services/server/src/controllers/subFolderController.ts @@ -154,11 +154,11 @@ export class SubFolderController extends Controller { @Path() folderName: string, @Path() subFolderName: string, ) { - return new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const objects: string[] = []; const stream = minioClient.listObjectsV2( "ehr", - `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, + `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`, true, ); @@ -168,10 +168,31 @@ export class SubFolderController extends Controller { objects.push(v.name); }); - stream.on("close", () => minioClient.removeObjects("ehr", objects)); + stream.on("close", async () => { + minioClient.removeObjects("ehr", objects); + resolve(); + }); stream.on("error", () => reject(new Error("Object storage error occured."))); - - resolve(this.setStatus(HttpStatusCode.NO_CONTENT)); }); + + const searchResult = await esClient.search({ + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + query: { + prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}` }, + }, + }); + + await Promise.all( + searchResult.hits.hits.map(async (v) => { + return esClient + .delete({ + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + id: v._id, + }) + .catch((e) => console.error(`ElasticSearch Error: ${e}`)); + }), + ); + + return this.setStatus(HttpStatusCode.NO_CONTENT); } } From 34c3f274184e45addce1c609de6772dc3df09261 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:49:08 +0700 Subject: [PATCH 02/29] feat: auth role --- Services/server/src/utils/auth.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Services/server/src/utils/auth.ts b/Services/server/src/utils/auth.ts index 407238a..20cf11f 100644 --- a/Services/server/src/utils/auth.ts +++ b/Services/server/src/utils/auth.ts @@ -17,7 +17,7 @@ const jwtVerify = createVerifier({ export function expressAuthentication( request: express.Request, securityName: string, - _scopes?: string[], + scopes?: string[], ) { return new Promise(async (resolve, reject) => { if (securityName !== "bearerAuth") reject(new Error("Unknown authentication method.")); @@ -34,6 +34,12 @@ export function expressAuthentication( return reject(new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.")); } + if (scopes && !scopes.every((v) => payload.resource_access[payload.azp].roles.includes(v))) { + return reject( + new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action."), + ); + } + return resolve(payload); }); } From 093eb7df8512e04f07a968b45a9a1f5d0d42c491 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:51:24 +0700 Subject: [PATCH 03/29] feat: add rabbitmq handler (not implemented) --- Services/server/src/rabbitmq/handler.ts | 103 ++++++++++++++++++++++++ Services/server/src/rabbitmq/index.ts | 38 +++++++++ 2 files changed, 141 insertions(+) create mode 100644 Services/server/src/rabbitmq/handler.ts create mode 100644 Services/server/src/rabbitmq/index.ts diff --git a/Services/server/src/rabbitmq/handler.ts b/Services/server/src/rabbitmq/handler.ts new file mode 100644 index 0000000..298a130 --- /dev/null +++ b/Services/server/src/rabbitmq/handler.ts @@ -0,0 +1,103 @@ +import { EhrFile } from "../interfaces/ehr-fs"; +import esClient from "../elasticsearch"; +import minioClient from "../storage"; + +const cachedBuffer: Record = {}; + +export async function handler(key: string): Promise { + console.info(`[AMQ] Messages received - key: ${key}`); + + const [bucket, ...fragment] = key.split("/"); + const pathname = fragment.join("/"); + + if (!cachedBuffer[key]) { + const stream = await minioClient.getObject(bucket, pathname); + const buffer = Buffer.concat(await stream.toArray()); + cachedBuffer[key] = buffer; + } + + const rec = await getInfo(pathname); + + const result = rec + ? await handleFoundRecord(rec, cachedBuffer[key]) + : await handleNotFoundRecord(pathname, cachedBuffer[key]); + + return result; +} + +// Get info and delete it from ElasticSearch to re-index +async function getInfo(pathname: string) { + const result = await esClient + .search }>({ + index: "my-test-index", + query: { match: { pathname } }, + }) + .catch((e) => console.error(e)); + + // pathname is unique and should not have multiple record with same path + if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) { + await esClient + .delete({ + index: "my-test-index", + id: result.hits.hits[0]._id, + }) + .catch((e) => console.error(e)); + + return result.hits.hits[0]._source; + } + + if (result && result.hits.hits.length === 0) console.info("Index Not Found"); + + return false; +} + +/** + * Handle when record in elasticsearch cannot be found. + * This will insert empty metadata. + */ +async function handleNotFoundRecord(pathname: string, buffer: Buffer) { + const filename = pathname.split("/").at(-1); + const base64 = Buffer.from(buffer).toString("base64"); + + const metadata = { + pathname, + fileName: filename ?? "n/a", // should not possible to fallback + fileSize: Buffer.byteLength(buffer), + fileType: "", + title: "", + description: "", + category: [], + keyword: [], + upload: true, + createdAt: new Date().toISOString(), + createdBy: "n/a", + updatedAt: new Date().toISOString(), + updatedBy: "n/a", + } satisfies Partial; + + const result = await esClient + .index({ + pipeline: "attachment", + index: "my-test-index", + document: { data: base64, ...metadata }, + }) + .catch((e) => console.error(e)); + + if (result) return true; + + return false; +} + +async function handleFoundRecord(metadata: EhrFile, buffer: Buffer) { + const result = await esClient + .index({ + pipeline: "attachment", + index: "my-test-index", + document: { data: Buffer.from(buffer).toString("base64"), ...metadata, upload: true }, + }) + .catch((e) => console.error(e)); + + if (result) return true; + + return false; +} diff --git a/Services/server/src/rabbitmq/index.ts b/Services/server/src/rabbitmq/index.ts new file mode 100644 index 0000000..2115eaf --- /dev/null +++ b/Services/server/src/rabbitmq/index.ts @@ -0,0 +1,38 @@ +import amqp from "amqplib"; + +export async function init(cb: (key: string) => boolean | Promise) { + if (!process.env.AMQ_URL || !process.env.AMQ_QUEUE) return; + + const { AMQ_URL: url, AMQ_QUEUE: queue } = process.env; + + const connection = await amqp.connect(url); + + const channel = await connection.createChannel(); + + channel.assertQueue(queue, { durable: true }); + channel.prefetch(1); + + console.log("[RabbitMQ] Listening for message..."); + + channel.consume( + queue, + async (msg) => { + if (!msg) return; + + const parsed: Record = JSON.parse(msg.content.toString()); + + if (typeof parsed.Key !== "string" || parsed.Key.includes(".keep")) return channel.ack(msg); + + const key = parsed.Key; + + if (await cb(key)) return channel.ack(msg); + + return await new Promise((resolve) => setTimeout(() => resolve(channel.nack(msg)), 3000)); + }, + { noAck: false }, + ); +} + +export default { + init, +}; From 21ee86ef4af47e9175f357575b6b80ea7ec68d45 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:51:38 +0700 Subject: [PATCH 04/29] chore: update doc string --- Services/server/src/utils/minio.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/Services/server/src/utils/minio.ts b/Services/server/src/utils/minio.ts index 3c3bc49..7d6be02 100644 --- a/Services/server/src/utils/minio.ts +++ b/Services/server/src/utils/minio.ts @@ -13,6 +13,7 @@ function safePath(path: string) { /** * Replace illegal character eg. ? % < > / \ : | that can't be in path with "-". + * Used when create folder / dir through api * @param path - string to check and replace * @returns path with illegal character replaced with "-" */ From 6be6a7256bb77636c63a797a2e0b74d76ca53c93 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:52:24 +0700 Subject: [PATCH 05/29] chore: format --- Services/server/src/controllers/searchController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/server/src/controllers/searchController.ts b/Services/server/src/controllers/searchController.ts index 5edb848..99de738 100644 --- a/Services/server/src/controllers/searchController.ts +++ b/Services/server/src/controllers/searchController.ts @@ -11,7 +11,7 @@ export class SearchController extends Controller { @SuccessResponse(HttpStatusCode.OK) public async searchFile(@Body() search: Search): Promise { const result = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", query: { bool: { must: search.AND?.map((v) => ({ match: { [v.field]: v.value } })), From 7cdbf140a9d4ff021e3fbba2ebc571160a5b747a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 24 Nov 2023 13:52:45 +0700 Subject: [PATCH 06/29] refactor: update type (add upload flag) --- Services/server/src/interfaces/ehr-fs.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Services/server/src/interfaces/ehr-fs.ts b/Services/server/src/interfaces/ehr-fs.ts index 919cbcc..77a3c9b 100644 --- a/Services/server/src/interfaces/ehr-fs.ts +++ b/Services/server/src/interfaces/ehr-fs.ts @@ -27,6 +27,11 @@ export interface EhrFile { category: string[]; keyword: string[]; + /** + * @private For internal use only. + */ + upload: boolean; + updatedAt: string | Date; updatedBy: string; createdAt: string | Date; From 9ca1213e500117b33413123b226dffbd0084e2fe Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 24 Nov 2023 14:46:34 +0700 Subject: [PATCH 07/29] fix: cache not clear --- Services/server/package.json | 3 + Services/server/pnpm-lock.yaml | 131 +++++++++++++++++++++++- Services/server/src/rabbitmq/handler.ts | 3 + 3 files changed, 136 insertions(+), 1 deletion(-) diff --git a/Services/server/package.json b/Services/server/package.json index 4e29ce5..20efe44 100644 --- a/Services/server/package.json +++ b/Services/server/package.json @@ -22,6 +22,7 @@ "@types/cors": "^2.8.16", "@types/jsonwebtoken": "^9.0.5", "@types/multer": "^1.4.10", + "amqplib": "^0.10.3", "concurrently": "^8.2.2", "cors": "^2.8.5", "dotenv": "^16.3.1", @@ -34,11 +35,13 @@ "tsoa": "^5.1.1" }, "devDependencies": { + "@types/amqplib": "^0.10.4", "@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", + "typedoc": "^0.25.3", "typescript": "^5.2.2" } } diff --git a/Services/server/pnpm-lock.yaml b/Services/server/pnpm-lock.yaml index 4bbe319..84c4764 100644 --- a/Services/server/pnpm-lock.yaml +++ b/Services/server/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: '@types/multer': specifier: ^1.4.10 version: 1.4.10 + amqplib: + specifier: ^0.10.3 + version: 0.10.3 concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -52,6 +55,9 @@ dependencies: version: 5.1.1 devDependencies: + '@types/amqplib': + specifier: ^0.10.4 + version: 0.10.4 '@types/express': specifier: ^4.17.21 version: 4.17.21 @@ -67,12 +73,26 @@ devDependencies: ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@20.9.0)(typescript@5.2.2) + typedoc: + specifier: ^0.25.3 + version: 0.25.3(typescript@5.2.2) typescript: specifier: ^5.2.2 version: 5.2.2 packages: + /@acuminous/bitsyntax@0.1.2: + resolution: {integrity: sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==} + engines: {node: '>=0.8'} + dependencies: + buffer-more-ints: 1.0.0 + debug: 4.3.4 + safe-buffer: 5.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/runtime@7.23.2: resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} engines: {node: '>=6.9.0'} @@ -181,6 +201,12 @@ packages: validator: 13.11.0 dev: false + /@types/amqplib@0.10.4: + resolution: {integrity: sha512-Y5Sqquh/LqDxSgxYaAAFNM0M7GyONtSDCcFMJk+DQwYEjibPyW6y+Yu9H9omdkKc3epyXULmFN3GTaeBHhn2Hg==} + dependencies: + '@types/node': 20.9.0 + dev: true + /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: @@ -295,11 +321,27 @@ packages: hasBin: true dev: true + /amqplib@0.10.3: + resolution: {integrity: sha512-UHmuSa7n8vVW/a5HGh2nFPqAEr8+cD4dEZ6u9GjP91nHfr1a54RyAKyra7Sb5NH7NBKOUlyQSMXIp0qAixKexw==} + engines: {node: '>=10'} + dependencies: + '@acuminous/bitsyntax': 0.1.2 + buffer-more-ints: 1.0.0 + readable-stream: 1.1.14 + url-parse: 1.5.10 + transitivePeerDependencies: + - supports-color + dev: false + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} dev: false + /ansi-sequence-parser@1.1.1: + resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} + dev: true + /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -430,7 +472,6 @@ packages: 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==} @@ -451,6 +492,10 @@ packages: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: false + /buffer-more-ints@1.0.0: + resolution: {integrity: sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==} + dev: false + /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1257,6 +1302,10 @@ packages: call-bind: 1.0.5 dev: false + /isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + dev: false + /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: false @@ -1280,6 +1329,10 @@ packages: resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==} dev: false + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -1299,10 +1352,20 @@ packages: yallist: 4.0.0 dev: true + /lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + dev: true + /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true + /marked@4.3.0: + resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} + engines: {node: '>= 12'} + hasBin: true + dev: true + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -1355,6 +1418,13 @@ packages: brace-expansion: 2.0.1 dev: false + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: false @@ -1566,6 +1636,10 @@ packages: strict-uri-encode: 2.0.0 dev: false + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + /range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -1581,6 +1655,15 @@ packages: unpipe: 1.0.0 dev: false + /readable-stream@1.1.14: + resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + dev: false + /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: @@ -1631,6 +1714,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: @@ -1743,6 +1830,15 @@ packages: resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} dev: false + /shiki@0.14.5: + resolution: {integrity: sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==} + dependencies: + ansi-sequence-parser: 1.1.1 + jsonc-parser: 3.2.0 + vscode-oniguruma: 1.7.0 + vscode-textmate: 8.0.0 + dev: true + /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -1832,6 +1928,10 @@ packages: es-abstract: 1.22.3 dev: false + /string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + dev: false + /string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: @@ -2014,6 +2114,20 @@ packages: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} dev: false + /typedoc@0.25.3(typescript@5.2.2): + resolution: {integrity: sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==} + engines: {node: '>= 16'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x + dependencies: + lunr: 2.3.9 + marked: 4.3.0 + minimatch: 9.0.3 + shiki: 0.14.5 + typescript: 5.2.2 + dev: true + /typescript@4.9.5: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} @@ -2067,6 +2181,13 @@ packages: engines: {node: '>= 0.8'} dev: false + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false @@ -2100,6 +2221,14 @@ packages: engines: {node: '>= 0.8'} dev: false + /vscode-oniguruma@1.7.0: + resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} + dev: true + + /vscode-textmate@8.0.0: + resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + dev: true + /web-encoding@1.1.5: resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} dependencies: diff --git a/Services/server/src/rabbitmq/handler.ts b/Services/server/src/rabbitmq/handler.ts index 298a130..0447faf 100644 --- a/Services/server/src/rabbitmq/handler.ts +++ b/Services/server/src/rabbitmq/handler.ts @@ -2,6 +2,7 @@ import { EhrFile } from "../interfaces/ehr-fs"; import esClient from "../elasticsearch"; import minioClient from "../storage"; +// for failed queue that will come later const cachedBuffer: Record = {}; export async function handler(key: string): Promise { @@ -22,6 +23,8 @@ export async function handler(key: string): Promise { ? await handleFoundRecord(rec, cachedBuffer[key]) : await handleNotFoundRecord(pathname, cachedBuffer[key]); + if (result) delete cachedBuffer[key]; + return result; } From 0a9f27a02e66f8111003a1ca88a1218e7981e795 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:04:36 +0700 Subject: [PATCH 08/29] feat: cache metadata on requeue --- Services/server/src/rabbitmq/handler.ts | 43 ++++++++++++++++++------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/Services/server/src/rabbitmq/handler.ts b/Services/server/src/rabbitmq/handler.ts index 0447faf..6ae21c1 100644 --- a/Services/server/src/rabbitmq/handler.ts +++ b/Services/server/src/rabbitmq/handler.ts @@ -4,6 +4,7 @@ import minioClient from "../storage"; // for failed queue that will come later const cachedBuffer: Record = {}; +const cachedMetadata: Record = {}; export async function handler(key: string): Promise { console.info(`[AMQ] Messages received - key: ${key}`); @@ -17,19 +18,27 @@ export async function handler(key: string): Promise { cachedBuffer[key] = buffer; } - const rec = await getInfo(pathname); + if (!cachedMetadata[key]) { + const stat = await minioClient.statObject(bucket, pathname); + cachedMetadata[key] = { size: stat.size, type: stat.metaData["content-type"] }; + } + + const rec = await popInfo(pathname); const result = rec - ? await handleFoundRecord(rec, cachedBuffer[key]) - : await handleNotFoundRecord(pathname, cachedBuffer[key]); + ? await handleFoundRecord(rec, cachedBuffer[key], cachedMetadata[key]) + : await handleNotFoundRecord(pathname, cachedBuffer[key], cachedMetadata[key]); - if (result) delete cachedBuffer[key]; + if (result) { + delete cachedBuffer[key]; + delete cachedMetadata[key]; + } return result; } // Get info and delete it from ElasticSearch to re-index -async function getInfo(pathname: string) { +async function popInfo(pathname: string) { const result = await esClient .search }>({ index: "my-test-index", @@ -58,15 +67,19 @@ async function getInfo(pathname: string) { * Handle when record in elasticsearch cannot be found. * This will insert empty metadata. */ -async function handleNotFoundRecord(pathname: string, buffer: Buffer) { +async function handleNotFoundRecord( + pathname: string, + buffer: Buffer, + stat: { size: number; type: string }, +) { const filename = pathname.split("/").at(-1); const base64 = Buffer.from(buffer).toString("base64"); const metadata = { pathname, - fileName: filename ?? "n/a", // should not possible to fallback - fileSize: Buffer.byteLength(buffer), - fileType: "", + fileName: filename ?? "n/a", // should not possible to fallback, just in case. + fileSize: stat.size, + fileType: stat.type, title: "", description: "", category: [], @@ -91,12 +104,20 @@ async function handleNotFoundRecord(pathname: string, buffer: Buffer) { return false; } -async function handleFoundRecord(metadata: EhrFile, buffer: Buffer) { +async function handleFoundRecord( + metadata: EhrFile, + buffer: Buffer, + stat: { size: number; type: string }, +) { + metadata.fileSize = stat.size; + metadata.fileType = stat.type; + metadata.upload = true; + const result = await esClient .index({ pipeline: "attachment", index: "my-test-index", - document: { data: Buffer.from(buffer).toString("base64"), ...metadata, upload: true }, + document: { data: Buffer.from(buffer).toString("base64"), ...metadata }, }) .catch((e) => console.error(e)); From 24350a11a4f035c74c5783bdc889b0386aca59c1 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Fri, 24 Nov 2023 21:11:11 +0700 Subject: [PATCH 09/29] fix: missing trailing slash --- Services/server/src/controllers/folderController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index fe554cb..fd79c91 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -83,7 +83,7 @@ export class FolderController extends Controller { @Path() drawerName: string, @Path() folderName: string, ) { - const fullpath = `${cabinetName}/${drawerName}/${folderName}`; + const fullpath = `${cabinetName}/${drawerName}/${folderName}/`; const list = await listItem(fullpath, true); From 3fc70daed072e6a791f36af7961c19dd8c12d65b Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 09:45:30 +0700 Subject: [PATCH 10/29] refactor: rabbitmq implement --- Services/server/src/app.ts | 4 + .../src/controllers/cabinetController.ts | 130 +++---- .../src/controllers/drawerController.ts | 153 ++++---- .../src/controllers/folderController.ts | 164 ++++---- .../src/controllers/subFolderController.ts | 168 ++++---- .../server/src/{storage => minio}/index.ts | 0 Services/server/src/rabbitmq/handler.ts | 36 +- Services/server/src/rabbitmq/index.ts | 12 +- Services/server/src/routes.ts | 43 +-- Services/server/src/swagger.json | 365 +++++++++++++----- Services/server/src/utils/auth.ts | 34 +- Services/server/src/utils/minio.ts | 112 +++--- 12 files changed, 676 insertions(+), 545 deletions(-) rename Services/server/src/{storage => minio}/index.ts (100%) diff --git a/Services/server/src/app.ts b/Services/server/src/app.ts index efefe83..24231e7 100644 --- a/Services/server/src/app.ts +++ b/Services/server/src/app.ts @@ -5,8 +5,10 @@ import cors from "cors"; import { RegisterRoutes } from "./routes"; import errorHandler from "./middlewares/exception"; +import rabbitmq from "./rabbitmq"; import swaggerSpecs from "./swagger.json"; +import { handler as amqHandler } from "./rabbitmq/handler"; const PORT = +(process.env.PORT || 80); @@ -28,3 +30,5 @@ app.use(errorHandler); app.listen(PORT, "0.0.0.0", () => console.log(`Application is running on http://localhost:${PORT}`), ); + +rabbitmq.init(amqHandler).catch((e) => console.error(e)); diff --git a/Services/server/src/controllers/cabinetController.ts b/Services/server/src/controllers/cabinetController.ts index 2aeab4d..fadac3d 100644 --- a/Services/server/src/controllers/cabinetController.ts +++ b/Services/server/src/controllers/cabinetController.ts @@ -11,104 +11,109 @@ import { SuccessResponse, Tags, Request, + Response, } from "tsoa"; -import * as Minio from "minio"; -import minioClient from "../storage"; -import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; -import HttpStatusCode from "../interfaces/http-status"; -import { listFolder, listItem, replaceIllegalChars } from "../utils/minio"; +import minioClient from "../minio"; import esClient from "../elasticsearch"; +import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; + +import HttpStatusCode from "../interfaces/http-status"; +import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); + @Route("cabinet") export class CabinetController extends Controller { @Get("/") @Tags("Cabinet") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @Response( + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง", + ) + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async listCabinet(): Promise { - const list = await listFolder().catch((e) => console.error(`Error List Folder: ${e}`)); - - if (!list) { - throw new Error("Error listing folder"); - } - + const list = await listFolder(DEFAULT_BUCKET!).catch((e) => + console.error(`Error List Folder: ${e}`), + ); + if (!list) + throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง"); return list; } @Post("/") @Tags("Cabinet") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.CREATED) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") + @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createCabinet( @Request() request: { user: { preferred_username: string } }, @Body() body: { name: string }, ) { - const uploaded = await minioClient - .putObject("ehr", `${replaceIllegalChars(body.name)}/.keep`, "", 0, { + const created = await minioClient + .putObject(DEFAULT_BUCKET!, `${replaceIllegalChars(body.name)}/.keep`, "", 0, { createdAt: new Date().toISOString(), createdBy: request.user.preferred_username, }) .catch((e) => console.error(e)); - if (!uploaded) throw new Error("Object storage error occured."); + if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); return this.setStatus(HttpStatusCode.CREATED); } @Put("/{cabinetName}") @Tags("Cabinet") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT, "Success") + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editCabinet( @Path() cabinetName: string, @Body() body: { name: string }, ): Promise { - const list = await listItem(`${cabinetName}/`, true); - - const cond = new Minio.CopyConditions(); + const path = `${cabinetName}/`; + const list = await listItem(DEFAULT_BUCKET!, path, true); await Promise.all( list.map(async (current) => { if (!current.name) return; - const destination = `${replaceIllegalChars(body.name)}/${current.name.slice( - cabinetName.length + 1, - )}`; - const source = `/ehr/${current.name}`; + const destination = `${replaceIllegalChars(body.name)}/${current.name.slice(path.length)}`; + const source = `/${DEFAULT_BUCKET}/${current.name}`; return await minioClient - .copyObject("ehr", destination, source, cond) + .copyObject(DEFAULT_BUCKET!, destination, source, copyCond) .then(async () => { - if (!current.name) return; - - await minioClient.removeObject("ehr", current.name); - - if (current.name.includes(".keep")) return; + if (current.name.includes(".keep")) { + return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); + } const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - match: { - pathname: current.name, - }, - }, + index: DEFAULT_INDEX!, + query: { match: { pathname: current.name } }, }); - if (search && search.hits.hits.length === 0) { - throw new Error("Data cannot be found in database."); - } + if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล"); const data = search.hits.hits[0]; await esClient.update({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + index: DEFAULT_INDEX!, id: data._id, doc: { pathname: destination }, }); + + await minioClient.removeObject(DEFAULT_BUCKET!, current.name); }) .catch((e) => { console.error(e); - throw new Error("Failed to move."); + throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้"); }); }), ); @@ -118,44 +123,23 @@ export class CabinetController extends Controller { @Delete("/{cabinetName}") @Tags("Cabinet") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteCabinet(@Path() cabinetName: string) { await new Promise((resolve, reject) => { const objects: string[] = []; - const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/`, true); + const stream = minioClient.listObjectsV2(DEFAULT_BUCKET!, `${cabinetName}/`, true); stream.on("data", (v) => { - if (!(v && v.name)) return; - - objects.push(v.name); + if (v && v.name) objects.push(v.name); }); - - stream.on("close", async () => { - minioClient.removeObjects("ehr", objects); - resolve(); - }); - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("close", async () => + resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)), + ); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); - const searchResult = await esClient.search({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - prefix: { pathname: `${cabinetName}/` }, - }, - }); - - await Promise.all( - searchResult.hits.hits.map(async (v) => { - return esClient - .delete({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - id: v._id, - }) - .catch((e) => console.error(`ElasticSearch Error: ${e}`)); - }), - ); - return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/controllers/drawerController.ts b/Services/server/src/controllers/drawerController.ts index e9fcf83..6afb601 100644 --- a/Services/server/src/controllers/drawerController.ts +++ b/Services/server/src/controllers/drawerController.ts @@ -6,120 +6,133 @@ import { Path, Post, Put, - Request, Route, Security, SuccessResponse, Tags, + Request, + Response, } from "tsoa"; -import * as Minio from "minio"; -import minioClient from "../storage"; + +import minioClient from "../minio"; +import esClient from "../elasticsearch"; + +import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; import HttpStatusCode from "../interfaces/http-status"; -import HttpError from "../interfaces/http-error"; -import { listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio"; -import esClient from "../elasticsearch"; import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; +import HttpError from "../interfaces/http-error"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); @Route("/cabinet/{cabinetName}/drawer") export class DrawerController extends Controller { @Get("/") @Tags("Drawer") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @Response( + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง", + ) + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async listDrawer(@Path() cabinetName: string): Promise { - const fullpath = [cabinetName, ""].join("/"); - - if (!(await pathExist(fullpath))) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); - } - - return listFolder(fullpath); + const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/`).catch((e) => + console.error(`Error List Folder: ${e}`), + ); + if (!list) + throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง"); + return list; } @Post("/") @Tags("Drawer") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.CREATED) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.NOT_FOUND, "ไม่พบลิ้นชัก") + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") + @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") 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."); + const basePath = `${cabinetName}/`; + + if ( + !Boolean( + await minioClient.statObject(DEFAULT_BUCKET!, `${basePath}.keep`).catch((e) => { + if (e.code === "NotFound") return false; + throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ) + ) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบลิ้นชัก"); } - const uploaded = await minioClient - .putObject("ehr", `${cabinetName}/${replaceIllegalChars(body.name)}/.keep`, "", 0, { + const created = await minioClient + .putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, { createdAt: new Date().toISOString(), createdBy: request.user.preferred_username, }) .catch((e) => console.error(e)); - if (!uploaded) { - throw new Error("Object storage error occured."); - } + if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); return this.setStatus(HttpStatusCode.CREATED); } @Put("/{drawerName}") @Tags("Drawer") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editDrawer( @Path() cabinetName: string, @Path() drawerName: string, @Body() body: { name: string }, ): Promise { - const fullpath = `${cabinetName}/${drawerName}/`; - - const list = await listItem(fullpath, true); - - const cond = new Minio.CopyConditions(); + const path = `${cabinetName}/${drawerName}/`; + const list = await listItem(DEFAULT_BUCKET!, path, true); await Promise.all( list.map(async (current) => { if (!current.name) return; const destination = `${cabinetName}/${replaceIllegalChars(body.name)}/${current.name.slice( - fullpath.length, + path.length, )}`; - const source = `/ehr/${current.name}`; + const source = `/${DEFAULT_BUCKET}/${current.name}`; return await minioClient - .copyObject("ehr", destination, source, cond) + .copyObject(DEFAULT_BUCKET!, destination, source, copyCond) .then(async () => { - if (!current.name) return; - - await minioClient.removeObject("ehr", current.name); - - if (current.name.includes(".keep")) return; + if (current.name.includes(".keep")) { + return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); + } const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - match: { - pathname: current.name, - }, - }, + index: DEFAULT_INDEX!, + query: { match: { pathname: current.name } }, }); - if (search && search.hits.hits.length === 0) { - throw new Error("Data cannot be found in database."); - } + if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล"); const data = search.hits.hits[0]; await esClient.update({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + index: DEFAULT_INDEX!, id: data._id, doc: { pathname: destination }, }); + + await minioClient.removeObject(DEFAULT_BUCKET!, current.name); }) .catch((e) => { console.error(e); - throw new Error("Failed to move."); + throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้"); }); }), ); @@ -129,44 +142,26 @@ export class DrawerController extends Controller { @Delete("/{drawerName}") @Tags("Drawer") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) { await new Promise((resolve, reject) => { const objects: string[] = []; - const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/${drawerName}/`, true); + const stream = minioClient.listObjectsV2( + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/`, + true, + ); stream.on("data", (v) => { - if (!(v && v.name)) return; - - objects.push(v.name); + if (v && v.name) objects.push(v.name); }); - - stream.on("close", async () => { - minioClient.removeObjects("ehr", objects); - resolve(); - }); - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("close", async () => + resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)), + ); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); - const searchResult = await esClient.search({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - prefix: { pathname: `${cabinetName}/${drawerName}/` }, - }, - }); - - await Promise.all( - searchResult.hits.hits.map(async (v) => { - return esClient - .delete({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - id: v._id, - }) - .catch((e) => console.error(`ElasticSearch Error: ${e}`)); - }), - ); - return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index fd79c91..f838b6c 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -6,88 +6,100 @@ import { Path, Post, Put, - Request, Route, Security, SuccessResponse, Tags, + Request, + Response, } from "tsoa"; -import * as Minio from "minio"; -import HttpError from "../interfaces/http-error"; -import HttpStatusCode from "../interfaces/http-status"; -import { listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio"; -import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; -import minioClient from "../storage"; +import minioClient from "../minio"; import esClient from "../elasticsearch"; +import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; + +import HttpStatusCode from "../interfaces/http-status"; +import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; +import HttpError from "../interfaces/http-error"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); + @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder") export class FolderController extends Controller { @Get("/") @Tags("Folder") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @Response( + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง", + ) + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async listFolder( @Path() cabinetName: string, @Path() drawerName: string, ): Promise { - const fullpath = [cabinetName, drawerName, ""].join("/"); - - if (!(await pathExist(fullpath))) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); - } - - return listFolder(fullpath); + const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/${drawerName}`).catch((e) => + console.error(`Error List Folder: ${e}`), + ); + if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง"); + return list; } @Post("/") @Tags("Folder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.CREATED) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม") + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") + @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createFolder( @Request() request: { user: { preferred_username: string } }, @Body() body: { name: string }, @Path() cabinetName: string, @Path() drawerName: string, ) { - if (!(await pathExist(`${cabinetName}/${drawerName}/`))) { - throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet or drawer cannot be found."); + const basePath = `${cabinetName}/${drawerName}/`; + + if ( + !Boolean( + await minioClient.statObject(DEFAULT_BUCKET!, `${basePath}.keep`).catch((e) => { + if (e.code === "NotFound") return false; + throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ) + ) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบแฟ้ม"); } - const uploaded = await minioClient - .putObject( - "ehr", - `${cabinetName}/${drawerName}/${replaceIllegalChars(body.name)}/.keep`, - "", - 0, - { - createdAt: new Date().toISOString(), - createdBy: request.user.preferred_username, - }, - ) + const created = await minioClient + .putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, { + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + }) .catch((e) => console.error(e)); - if (!uploaded) { - throw new Error("Object storage error occured."); - } + if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); return this.setStatus(HttpStatusCode.CREATED); } @Put("/{folderName}") @Tags("Folder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editFolder( @Body() body: { name: string }, @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, ) { - const fullpath = `${cabinetName}/${drawerName}/${folderName}/`; - - const list = await listItem(fullpath, true); - - const cond = new Minio.CopyConditions(); + const path = `${cabinetName}/${drawerName}/${folderName}`; + const list = await listItem(DEFAULT_BUCKET!, path, true); await Promise.all( list.map(async (current) => { @@ -95,42 +107,36 @@ export class FolderController extends Controller { const destination = `${cabinetName}/${drawerName}/${replaceIllegalChars( body.name, - )}/${current.name.slice(fullpath.length)}`; - const source = `/ehr/${current.name}`; + )}/${current.name.slice(path.length)}`; + const source = `/${DEFAULT_BUCKET}/${current.name}`; return await minioClient - .copyObject("ehr", destination, source, cond) + .copyObject(DEFAULT_BUCKET!, destination, source, copyCond) .then(async () => { - if (!current.name) return; - - await minioClient.removeObject("ehr", current.name); - - if (current.name.includes(".keep")) return; + if (current.name.includes(".keep")) { + return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); + } const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - match: { - pathname: current.name, - }, - }, + index: DEFAULT_INDEX!, + query: { match: { pathname: current.name } }, }); - if (search && search.hits.hits.length === 0) { - throw new Error("Data cannot be found in database."); - } + if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล"); const data = search.hits.hits[0]; await esClient.update({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + index: DEFAULT_INDEX!, id: data._id, doc: { pathname: destination }, }); + + await minioClient.removeObject(DEFAULT_BUCKET!, current.name); }) .catch((e) => { console.error(e); - throw new Error("Failed to move."); + throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้"); }); }), ); @@ -140,8 +146,8 @@ export class FolderController extends Controller { @Delete("/{folderName}") @Tags("Folder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteFolder( @Path() cabinetName: string, @Path() drawerName: string, @@ -150,42 +156,20 @@ export class FolderController extends Controller { await new Promise((resolve, reject) => { const objects: string[] = []; const stream = minioClient.listObjectsV2( - "ehr", - `${cabinetName}/${drawerName}/${folderName}/`, + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}`, true, ); stream.on("data", (v) => { - if (!(v && v.name)) return; - - objects.push(v.name); + if (v && v.name) objects.push(v.name); }); - - stream.on("close", async () => { - minioClient.removeObjects("ehr", objects); - resolve(); - }); - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("close", async () => + resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)), + ); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); - const searchResult = await esClient.search({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/` }, - }, - }); - - await Promise.all( - searchResult.hits.hits.map(async (v) => { - return esClient - .delete({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - id: v._id, - }) - .catch((e) => console.error(`ElasticSearch Error: ${e}`)); - }), - ); - return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/controllers/subFolderController.ts b/Services/server/src/controllers/subFolderController.ts index f54a79c..376bae8 100644 --- a/Services/server/src/controllers/subFolderController.ts +++ b/Services/server/src/controllers/subFolderController.ts @@ -6,44 +6,58 @@ import { Path, Post, Put, - Request, Route, Security, SuccessResponse, Tags, + Request, + Response, } from "tsoa"; -import * as Minio from "minio"; -import HttpError from "../interfaces/http-error"; -import HttpStatusCode from "../interfaces/http-status"; -import { listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio"; -import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; -import minioClient from "../storage"; +import minioClient from "../minio"; import esClient from "../elasticsearch"; +import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; + +import HttpStatusCode from "../interfaces/http-status"; +import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; +import HttpError from "../interfaces/http-error"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); + @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") export class SubFolderController extends Controller { @Get("/") @Tags("SubFolder") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @Response( + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง", + ) + @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); + const list = await listFolder( + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}`, + ).catch((e) => console.error(`Error List Folder: ${e}`)); + if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง"); + return list; } @Post("/") @Tags("SubFolder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.CREATED) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม") + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") + @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createFolder( @Request() request: { user: { preferred_username: string } }, @Body() body: { name: string }, @@ -51,37 +65,36 @@ export class SubFolderController extends Controller { @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 basePath = `${cabinetName}/${drawerName}/${folderName}/`; + + if ( + !Boolean( + await minioClient.statObject(DEFAULT_BUCKET!, `${basePath}.keep`).catch((e) => { + if (e.code === "NotFound") return false; + throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ) + ) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบแฟ้ม"); } - const uploaded = await minioClient - .putObject( - "ehr", - `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(body.name)}/.keep`, - "", - 0, - { - createdAt: new Date().toISOString(), - createdBy: request.user.preferred_username, - }, - ) + const created = await minioClient + .putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, { + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + }) .catch((e) => console.error(e)); - if (!uploaded) { - throw new Error("Object storage error occured."); - } + if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); return this.setStatus(HttpStatusCode.CREATED); } @Put("/{subFolderName}") @Tags("SubFolder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editFolder( @Body() body: { name: string }, @Path() cabinetName: string, @@ -89,11 +102,8 @@ export class SubFolderController extends Controller { @Path() folderName: string, @Path() subFolderName: string, ) { - const fullpath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`; - - const list = await listItem(fullpath, true); - - const cond = new Minio.CopyConditions(); + const path = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`; + const list = await listItem(DEFAULT_BUCKET!, path, true); await Promise.all( list.map(async (current) => { @@ -101,42 +111,36 @@ export class SubFolderController extends Controller { const destination = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars( body.name, - )}/${current.name.slice(fullpath.length)}`; - const source = `/ehr/${current.name}`; + )}/${current.name.slice(path.length)}`; + const source = `/${DEFAULT_BUCKET}/${current.name}`; return await minioClient - .copyObject("ehr", destination, source, cond) + .copyObject(DEFAULT_BUCKET!, destination, source, copyCond) .then(async () => { - if (!current.name) return; - - await minioClient.removeObject("ehr", current.name); - - if (current.name.includes(".keep")) return; + if (current.name.includes(".keep")) { + return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); + } const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - match: { - pathname: current.name, - }, - }, + index: DEFAULT_INDEX!, + query: { match: { pathname: current.name } }, }); - if (search && search.hits.hits.length === 0) { - throw new Error("Data cannot be found in database."); - } + if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล"); const data = search.hits.hits[0]; await esClient.update({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + index: DEFAULT_INDEX!, id: data._id, doc: { pathname: destination }, }); + + await minioClient.removeObject(DEFAULT_BUCKET!, current.name); }) .catch((e) => { console.error(e); - throw new Error("Failed to move."); + throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้"); }); }), ); @@ -146,8 +150,8 @@ export class SubFolderController extends Controller { @Delete("/{subFolderName}") @Tags("SubFolder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteFolder( @Path() cabinetName: string, @Path() drawerName: string, @@ -157,42 +161,20 @@ export class SubFolderController extends Controller { await new Promise((resolve, reject) => { const objects: string[] = []; const stream = minioClient.listObjectsV2( - "ehr", - `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`, + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, true, ); stream.on("data", (v) => { - if (!(v && v.name)) return; - - objects.push(v.name); + if (v && v.name) objects.push(v.name); }); - - stream.on("close", async () => { - minioClient.removeObjects("ehr", objects); - resolve(); - }); - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("close", async () => + resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)), + ); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); - const searchResult = await esClient.search({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}` }, - }, - }); - - await Promise.all( - searchResult.hits.hits.map(async (v) => { - return esClient - .delete({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - id: v._id, - }) - .catch((e) => console.error(`ElasticSearch Error: ${e}`)); - }), - ); - return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/storage/index.ts b/Services/server/src/minio/index.ts similarity index 100% rename from Services/server/src/storage/index.ts rename to Services/server/src/minio/index.ts diff --git a/Services/server/src/rabbitmq/handler.ts b/Services/server/src/rabbitmq/handler.ts index 6ae21c1..2a74aa6 100644 --- a/Services/server/src/rabbitmq/handler.ts +++ b/Services/server/src/rabbitmq/handler.ts @@ -1,17 +1,25 @@ import { EhrFile } from "../interfaces/ehr-fs"; import esClient from "../elasticsearch"; -import minioClient from "../storage"; +import minioClient from "../minio"; + +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); // for failed queue that will come later const cachedBuffer: Record = {}; const cachedMetadata: Record = {}; -export async function handler(key: string): Promise { - console.info(`[AMQ] Messages received - key: ${key}`); +export async function handler(key: string, event: string): Promise { + console.info(`[AMQ] Messages received - key: ${key}, event: ${event}`); const [bucket, ...fragment] = key.split("/"); const pathname = fragment.join("/"); + if (event === "s3:ObjectRemoved:Delete") { + return await ensureDelete(pathname); + } + if (!cachedBuffer[key]) { const stream = await minioClient.getObject(bucket, pathname); const buffer = Buffer.concat(await stream.toArray()); @@ -41,7 +49,7 @@ export async function handler(key: string): Promise { async function popInfo(pathname: string) { const result = await esClient .search }>({ - index: "my-test-index", + index: DEFAULT_INDEX!, query: { match: { pathname } }, }) .catch((e) => console.error(e)); @@ -50,7 +58,7 @@ async function popInfo(pathname: string) { if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) { await esClient .delete({ - index: "my-test-index", + index: DEFAULT_INDEX!, id: result.hits.hits[0]._id, }) .catch((e) => console.error(e)); @@ -63,6 +71,20 @@ async function popInfo(pathname: string) { return false; } +/** + * If there is data in database then delete it + */ +async function ensureDelete(pathname: string) { + await esClient + .deleteByQuery({ + index: DEFAULT_INDEX!, + query: { match: { pathname } }, + conflicts: "proceed", + }) + .catch((e) => console.error(e)); + return true; +} + /** * Handle when record in elasticsearch cannot be found. * This will insert empty metadata. @@ -94,7 +116,7 @@ async function handleNotFoundRecord( const result = await esClient .index({ pipeline: "attachment", - index: "my-test-index", + index: DEFAULT_INDEX!, document: { data: base64, ...metadata }, }) .catch((e) => console.error(e)); @@ -116,7 +138,7 @@ async function handleFoundRecord( const result = await esClient .index({ pipeline: "attachment", - index: "my-test-index", + index: DEFAULT_INDEX!, document: { data: Buffer.from(buffer).toString("base64"), ...metadata }, }) .catch((e) => console.error(e)); diff --git a/Services/server/src/rabbitmq/index.ts b/Services/server/src/rabbitmq/index.ts index 2115eaf..8f5a1f1 100644 --- a/Services/server/src/rabbitmq/index.ts +++ b/Services/server/src/rabbitmq/index.ts @@ -1,6 +1,6 @@ import amqp from "amqplib"; -export async function init(cb: (key: string) => boolean | Promise) { +export async function init(cb: (key: string, event: string) => boolean | Promise) { if (!process.env.AMQ_URL || !process.env.AMQ_QUEUE) return; const { AMQ_URL: url, AMQ_QUEUE: queue } = process.env; @@ -22,10 +22,14 @@ export async function init(cb: (key: string) => boolean | Promise) { const parsed: Record = JSON.parse(msg.content.toString()); if (typeof parsed.Key !== "string" || parsed.Key.includes(".keep")) return channel.ack(msg); + if (typeof parsed.EventName !== "string" || parsed.EventName.includes("Copy")) { + return channel.ack(msg); + } const key = parsed.Key; + const event = parsed.EventName; - if (await cb(key)) return channel.ack(msg); + if (await cb(key, event)) return channel.ack(msg); return await new Promise((resolve) => setTimeout(() => resolve(channel.nack(msg)), 3000)); }, @@ -33,6 +37,4 @@ export async function init(cb: (key: string) => boolean | Promise) { ); } -export default { - init, -}; +export default { init }; diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index 2ee984d..689bd83 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -48,6 +48,7 @@ const models: TsoaRoute.Models = { "description": {"dataType":"string","required":true}, "category": {"dataType":"array","array":{"dataType":"string"},"required":true}, "keyword": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "upload": {"dataType":"boolean","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}, @@ -76,6 +77,7 @@ export function RegisterRoutes(app: Router) { // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa // ########################################################################################################### app.get('/cabinet', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.listCabinet)), @@ -100,7 +102,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.post('/cabinet', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.createCabinet)), @@ -127,7 +129,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":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.editCabinet)), @@ -154,7 +156,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":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.deleteCabinet)), @@ -180,6 +182,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', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.listDrawer)), @@ -205,7 +208,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.post('/cabinet/:cabinetName/drawer', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.createDrawer)), @@ -233,7 +236,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":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.editDrawer)), @@ -261,7 +264,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":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.deleteDrawer)), @@ -289,18 +292,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/file', authenticateMiddleware([{"bearerAuth":[]}]), - upload.single('file'), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.uploadFile)), 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"}, - 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"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string","required":true},"category":{"dataType":"string","required":true},"description":{"dataType":"string","required":true},"title":{"dataType":"string","required":true},"file":{"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"}, @@ -351,7 +349,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.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName', authenticateMiddleware([{"bearerAuth":[]}]), - upload.single('file'), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.updateFile)), @@ -362,11 +359,7 @@ export function RegisterRoutes(app: Router) { 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"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string"},"category":{"dataType":"string"},"description":{"dataType":"string"},"title":{"dataType":"string"},"file":{"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 @@ -443,6 +436,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', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.listFolder)), @@ -469,7 +463,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.post('/cabinet/:cabinetName/drawer/:drawerName/folder', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.createFolder)), @@ -498,7 +492,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":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.editFolder)), @@ -527,7 +521,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":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.deleteFolder)), @@ -580,6 +574,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)), @@ -607,7 +602,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.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.createFolder)), @@ -637,7 +632,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":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.editFolder)), @@ -667,7 +662,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":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.deleteFolder)), diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index d9bde06..6b57bcb 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -79,6 +79,9 @@ }, "type": "array" }, + "upload": { + "type": "boolean" + }, "updatedAt": { "anyOf": [ { @@ -117,6 +120,7 @@ "description", "category", "keyword", + "upload", "updatedAt", "updatedBy", "createdAt", @@ -178,9 +182,9 @@ } }, "info": { - "title": "BMA EHR - Test Service API", - "version": "0.0.1", - "description": "Best practice for initialize express project", + "title": "Enterprise Document Management(EDM) - API", + "version": "0.0.2", + "description": "Open API Specfication for Enterprise Document Management ", "license": { "name": "by Frappet", "url": "https://frappet.com" @@ -193,7 +197,7 @@ "operationId": "ListCabinet", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -204,19 +208,9 @@ } } } - } - }, - "tags": [ - "Cabinet" - ], - "security": [], - "parameters": [] - }, - "post": { - "operationId": "CreateCabinet", - "responses": { - "201": { - "description": "" + }, + "500": { + "description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง" } }, "tags": [ @@ -227,6 +221,28 @@ "bearerAuth": [] } ], + "parameters": [] + }, + "post": { + "operationId": "CreateCabinet", + "responses": { + "201": { + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์" + } + }, + "tags": [ + "Cabinet" + ], + "security": [ + { + "bearerAuth": [ + "admin" + ] + } + ], "parameters": [], "requestBody": { "required": true, @@ -253,7 +269,10 @@ "operationId": "EditCabinet", "responses": { "204": { - "description": "Success" + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้" } }, "tags": [ @@ -261,7 +280,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -297,12 +318,10 @@ "operationId": "DeleteCabinet", "responses": { "204": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้" } }, "tags": [ @@ -310,7 +329,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -330,7 +351,7 @@ "operationId": "ListDrawer", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -341,12 +362,19 @@ } } } + }, + "500": { + "description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง" } }, "tags": [ "Drawer" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -362,7 +390,13 @@ "operationId": "CreateDrawer", "responses": { "201": { - "description": "" + "description": "สำเร็จ" + }, + "404": { + "description": "ไม่พบลิ้นชัก" + }, + "500": { + "description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์" } }, "tags": [ @@ -370,7 +404,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -408,7 +444,10 @@ "operationId": "EditDrawer", "responses": { "204": { - "description": "" + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้" } }, "tags": [ @@ -416,7 +455,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -460,12 +501,7 @@ "operationId": "DeleteDrawer", "responses": { "204": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } + "description": "สำเร็จ" } }, "tags": [ @@ -473,7 +509,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -501,7 +539,74 @@ "operationId": "UploadFile", "responses": { "201": { - "description": "" + "description": "", + "content": { + "application/json": { + "schema": { + "properties": { + "keyword": { + "type": "string" + }, + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "file": { + "type": "string" + }, + "upload": { + "type": "string" + }, + "updatedBy": { + "type": "string" + }, + "updatedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "createdBy": { + "type": "string" + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + } + }, + "required": [ + "keyword", + "category", + "description", + "title", + "file", + "upload", + "updatedBy", + "updatedAt", + "createdBy", + "createdAt" + ], + "type": "object" + } + } + } } }, "tags": [ @@ -541,34 +646,33 @@ "requestBody": { "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { - "type": "object", "properties": { - "file": { - "type": "string", - "format": "binary" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, "keyword": { "type": "string" }, "category": { "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "file": { + "type": "string" } }, "required": [ - "file", - "title", - "description", "keyword", - "category" - ] + "category", + "description", + "title", + "file" + ], + "type": "object" } } } @@ -628,7 +732,27 @@ "operationId": "UpdateFile", "responses": { "200": { - "description": "" + "description": "", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "properties": { + "upload": { + "type": "string" + } + }, + "required": [ + "upload" + ], + "type": "object" + } + ] + } + } + } } }, "tags": [ @@ -674,29 +798,28 @@ } ], "requestBody": { - "required": false, + "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { - "type": "object", "properties": { - "file": { - "type": "string", - "format": "binary" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, "keyword": { "type": "string" }, "category": { "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "file": { + "type": "string" } - } + }, + "type": "object" } } } @@ -789,6 +912,9 @@ } ] }, + "upload": { + "type": "boolean" + }, "keyword": { "items": { "type": "string" @@ -829,6 +955,7 @@ "createdAt", "updatedBy", "updatedAt", + "upload", "keyword", "category", "description", @@ -890,7 +1017,7 @@ "operationId": "ListFolder", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -901,12 +1028,19 @@ } } } + }, + "500": { + "description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง" } }, "tags": [ "Folder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -930,7 +1064,13 @@ "operationId": "CreateFolder", "responses": { "201": { - "description": "" + "description": "สำเร็จ" + }, + "404": { + "description": "ไม่พบของแฟ้ม" + }, + "500": { + "description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์" } }, "tags": [ @@ -938,7 +1078,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -984,7 +1126,10 @@ "operationId": "EditFolder", "responses": { "204": { - "description": "" + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้" } }, "tags": [ @@ -992,7 +1137,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1044,12 +1191,7 @@ "operationId": "DeleteFolder", "responses": { "204": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } + "description": "สำเร็จ" } }, "tags": [ @@ -1057,7 +1199,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1128,7 +1272,7 @@ "operationId": "ListFolder", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -1139,12 +1283,19 @@ } } } + }, + "500": { + "description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง" } }, "tags": [ "SubFolder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -1176,7 +1327,13 @@ "operationId": "CreateFolder", "responses": { "201": { - "description": "" + "description": "สำเร็จ" + }, + "404": { + "description": "ไม่พบของแฟ้ม" + }, + "500": { + "description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์" } }, "tags": [ @@ -1184,7 +1341,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1238,7 +1397,10 @@ "operationId": "EditFolder", "responses": { "204": { - "description": "" + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้" } }, "tags": [ @@ -1246,7 +1408,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1306,12 +1470,7 @@ "operationId": "DeleteFolder", "responses": { "204": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } + "description": "สำเร็จ" } }, "tags": [ @@ -1319,7 +1478,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1482,6 +1643,9 @@ } ] }, + "upload": { + "type": "boolean" + }, "keyword": { "items": { "type": "string" @@ -1519,6 +1683,7 @@ "createdAt", "updatedBy", "updatedAt", + "upload", "keyword", "category", "description", @@ -1758,6 +1923,9 @@ } ] }, + "upload": { + "type": "boolean" + }, "keyword": { "items": { "type": "string" @@ -1798,6 +1966,7 @@ "createdAt", "updatedBy", "updatedAt", + "upload", "keyword", "category", "description", diff --git a/Services/server/src/utils/auth.ts b/Services/server/src/utils/auth.ts index 20cf11f..5d71bec 100644 --- a/Services/server/src/utils/auth.ts +++ b/Services/server/src/utils/auth.ts @@ -14,32 +14,30 @@ const jwtVerify = createVerifier({ }, }); -export function expressAuthentication( +export async function expressAuthentication( request: express.Request, securityName: string, scopes?: string[], ) { - return new Promise(async (resolve, reject) => { - if (securityName !== "bearerAuth") reject(new Error("Unknown authentication method.")); + if (process.env.AUTH_BYPASS) return { preferred_username: "bypassed" }; - const token = request.headers["authorization"]?.includes("Bearer ") - ? request.headers["authorization"].split(" ")[1] - : null; + if (securityName !== "bearerAuth") throw new Error("Unknown authentication method."); - if (!token) return reject(new HttpError(HttpStatusCode.UNAUTHORIZED, "No token provided.")); + const token = request.headers["authorization"]?.includes("Bearer ") + ? request.headers["authorization"].split(" ")[1] + : null; - const payload = await jwtVerify(token).catch((_) => null); + if (!token) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "No token provided."); - if (!payload) { - return reject(new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.")); - } + const payload = await jwtVerify(token).catch((_) => null); - if (scopes && !scopes.every((v) => payload.resource_access[payload.azp].roles.includes(v))) { - return reject( - new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action."), - ); - } + if (!payload) { + throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided."); + } - return resolve(payload); - }); + if (scopes && !scopes.some((v) => payload.resource_access[payload.azp].roles.includes(v))) { + throw new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action."); + } + + return payload; } diff --git a/Services/server/src/utils/minio.ts b/Services/server/src/utils/minio.ts index 7d6be02..53cf2b2 100644 --- a/Services/server/src/utils/minio.ts +++ b/Services/server/src/utils/minio.ts @@ -1,38 +1,27 @@ import { EhrFolder } from "../interfaces/ehr-fs"; import * as Minio from "minio"; -import minioClient from "../storage"; +import minioClient from "../minio"; /** - * 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 "-". - * Used when create folder / dir through api + * Replace illegal character eg. ? % < > / \ : | that can't be in path with other char with dash by default. * @param path - string to check and replace - * @returns path with illegal character replaced with "-" + * @param replace - string to replace illegal character + * @returns illegal character replaced path */ -export function replaceIllegalChars(path: string, replaceChar = "-") { - return path.replace(/[/\\?%*:|"<>]/g, replaceChar); +export function replaceIllegalChars(path: string, replace = "-") { + return path.replace(/[/\\?%*:|"<>]/g, replace); } /** - * Utility function to check for .keep file if it is exist or not. - * @returns true if .keep exist, false otherwise + * Check if folder really exist by using ".keep" object. */ export async function pathExist(path: string): Promise { - return await minioClient - .statObject("ehr", `${safePath(path)}.keep`) - .then((_) => true) - .catch((e) => { + return Boolean( + await minioClient.statObject("ehr", `${path.replace(/^\/|\/$/g, "")}/.keep`).catch((e) => { if (e.code === "NotFound") return false; - throw new Error("Object Storage Error"); - }); + throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ); } /** @@ -40,55 +29,62 @@ export async function pathExist(path: string): Promise { * @param path - path to list * @return list of folder with metadata */ -export function listFolder(path?: string): Promise { - if (path) path = safePath(path); +export async function listFolder(bucket: string, path?: string): Promise { + if (path) path = `${path.replace(/^\/|\/$/g, "")}/`; - return new Promise((resolve, reject) => { - const folder: EhrFolder[] = []; - - const stream = minioClient.listObjectsV2("ehr", path ?? ""); + const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => { + const item: { pathname: string; name: string }[] = []; + const stream = minioClient.listObjectsV2(bucket, 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`) - .catch((e) => console.error(`Error List Folder: ${folder[i].pathname}`, e)); - - if (!stat) continue; - - folder[i] = { - ...folder[i], - createdAt: stat.metaData.createdat ?? "N/A", - createdBy: stat.metaData.createdby ?? "N/A", - }; + if (v && v.prefix) { + item.push({ + pathname: v.prefix, + name: v.prefix.slice(path?.length).split("/")[0], + }); } - resolve(folder); }); - - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("end", () => resolve(item)); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"))); }); + + const folder = await Promise.all( + list.map(async (v) => { + // Get stat from hidden object that used to mark as folder as minio doesn't really have folder + const stat = await minioClient + .statObject(bucket, `${v.pathname}.keep`) + .catch((e) => console.error(`MinIO Error: ${e}`)); + + if (!stat) return undefined; + + const { createdat, createdby } = stat.metaData; + + return { + ...v, + createdAt: createdat ?? "n/a", + createdBy: createdby ?? "n/a", + } satisfies EhrFolder; + }), + ); + + return folder.filter((v: (typeof folder)[number]): v is EhrFolder => !!v); } -export async function listItem(path: string, recursive = false): Promise { +export async function listItem( + bucket: string, + path: string, + recursive = false, +): Promise { return new Promise((resolve, reject) => { - const stream = minioClient.listObjectsV2("ehr", path, recursive); + const stream = minioClient.listObjectsV2(bucket, path, recursive); const item: Minio.BucketItem[] = []; stream.on("data", (v) => { if (v && v.name) item.push(v); }); stream.on("end", () => resolve(item)); - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"))); }); } + +export const copyCond = new Minio.CopyConditions(); From 28f35e73289f4bbdbbf59c6e6825a1e417a62341 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:04:14 +0700 Subject: [PATCH 11/29] fix: wrong error message --- .../src/controllers/drawerController.ts | 2 +- .../server/src/controllers/fileController.ts | 344 +++++++++--------- .../src/controllers/folderController.ts | 4 +- 3 files changed, 171 insertions(+), 179 deletions(-) diff --git a/Services/server/src/controllers/drawerController.ts b/Services/server/src/controllers/drawerController.ts index 6afb601..329a1b4 100644 --- a/Services/server/src/controllers/drawerController.ts +++ b/Services/server/src/controllers/drawerController.ts @@ -69,7 +69,7 @@ export class DrawerController extends Controller { }), ) ) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบลิ้นชัก"); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างลิ้นชัก"); } const created = await minioClient diff --git a/Services/server/src/controllers/fileController.ts b/Services/server/src/controllers/fileController.ts index 6fb9393..310a008 100644 --- a/Services/server/src/controllers/fileController.ts +++ b/Services/server/src/controllers/fileController.ts @@ -1,7 +1,7 @@ import { + Body, Controller, Delete, - FormField, Get, Patch, Path, @@ -11,14 +11,22 @@ import { Security, SuccessResponse, Tags, - UploadedFile, } from "tsoa"; + import esClient from "../elasticsearch"; -import minioClient from "../storage"; +import minioClient from "../minio"; + import HttpStatusCode from "../interfaces/http-status"; -import { pathExist } from "../utils/minio"; -import HttpError from "../interfaces/http-error"; import { EhrFile } from "../interfaces/ehr-fs"; +import HttpError from "../interfaces/http-error"; + +import { copyCond, pathExist } from "../utils/minio"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file") export class FileController extends Controller { @@ -28,87 +36,57 @@ export class FileController extends Controller { @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, + @Body() + body: { + file: string; + title: string; + description: string; + category: string; + keyword: 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}`; + const pathname = `${cabinetName}/${drawerName}/${folderName}/${body.file}`; if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/`))) { throw new HttpError( - HttpStatusCode.PRECONDITION_FAILED, - "Cabinet, drawer or folder cannot be found.", + HttpStatusCode.NOT_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: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - query: { - match: { - pathname: pathname, - }, - }, - }); - - const exist = search.hits.hits.find((v) => v._source?.pathname === pathname); + const rec = await popInfo(pathname); const metadata: Partial = { pathname, - fileName: filename, - fileSize: file.size, - fileType: file.mimetype, - title: title, - description: description, - category: category.split(","), - keyword: keyword.split(","), + fileName: body.file, + fileSize: 0, + fileType: "", + title: body.title, + description: body.description, + category: body.category.split(","), + keyword: body.keyword.split(","), + upload: false, + createdAt: new Date().toISOString(), + createdBy: rec ? rec.createdBy : "n/a", + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username ?? "n/a", }; - if (!exist) { - await esClient.index({ - pipeline: "attachment", - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - 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: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - 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, - }, - }); - } + await esClient.index({ + index: "dev-index", + document: metadata, + }); - return this.setStatus(HttpStatusCode.CREATED); + return { + ...body, + createdAt: metadata.createdAt, + createdBy: metadata.createdBy, + updatedAt: metadata.updatedAt, + updatedBy: metadata.updatedBy, + upload: await minioClient.presignedPutObject("ehr", pathname), + }; } @Get("/") @@ -124,7 +102,7 @@ export class FileController extends Controller { attachment: Record; } >({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", query: { prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/`, @@ -132,16 +110,14 @@ 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) => { - if (!v._source) return; - - const { attachment, ...rest } = v._source; - - return rest; + if (v._source) { + const { attachment, ...rest } = v._source; + return rest satisfies EhrFile; + } }) - .flatMap((v) => (v ? [v] : [])); + .filter((v: EhrFile | undefined): v is EhrFile => !!v); return records; } @@ -156,86 +132,88 @@ export class FileController extends Controller { @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: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - query: { - match: { - pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, - }, - }, - }); + @Body() + body: { + file?: string; + title?: string; + description?: string; + category?: string; + keyword?: string; + }, + ): Promise { + const 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: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - 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}`, + 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, + // assume user will replace file by re-upload + if (body.file) { + const destination = `${cabinetName}/${drawerName}/${folderName}/${body.file}`; + const source = `ehr/${cabinetName}/${drawerName}/${folderName}/${fileName}`; + + const copy = await minioClient.copyObject("ehr", destination, source, copyCond); + + if (copy) { + const search = await esClient + .search }>({ + index: "my-test-index", + query: { match: { pathname } }, + }) + .catch((e) => console.error(e)); + + if (search && search.hits.hits.length > 0 && search.hits.hits[0]._source) { + const { _index: index, _id: id } = search.hits.hits[0]; + await esClient + .update({ + index, + id, + doc: { + pathname: destination, + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username ?? "n/a", + }, + }) + .then(() => minioClient.removeObject("ehr", pathname)); + } else { + await minioClient.removeObject("ehr", pathname); + } + } + } else { + const search = await esClient + .search }>({ + index: "my-test-index", + query: { match: { pathname } }, }) .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: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - 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, - }, - }); + if (search && search.hits.hits.length > 0 && search.hits.hits[0]._source) { + const { _index: index, _id: id } = search.hits.hits[0]; + await esClient.update({ + index, + id, + doc: { + ...body, + keyword: body.keyword?.split(","), + category: body.category?.split(","), + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username ?? "n/a", + }, + }); + } } - return this.setStatus(HttpStatusCode.NO_CONTENT); + return body.file + ? this.setStatus(HttpStatusCode.NO_CONTENT) + : { + upload: await minioClient.presignedPutObject( + "ehr", + `${cabinetName}/${drawerName}/${folderName}/${body.file ?? fileName}`, + ), + }; } @Delete("/{fileName}") @@ -248,31 +226,24 @@ export class FileController extends Controller { @Path() folderName: string, @Path() fileName: string, ) { - const search = await esClient.search< - EhrFile & { - attachment: Record; - } - >({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - query: { - match: { - pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, + const result = await esClient + .deleteByQuery({ + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + 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: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - id: search.hits.hits[0]._id, }) .catch((e) => console.error(e)); - if (!esResult) throw new Error("An error occured, cannot perform this action."); + if (result && result.total === 0) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "Data not found"); + } + + if (!result) { + throw new Error("An error occured, cannot perform this action."); + } await minioClient.removeObject("ehr", `${cabinetName}/${drawerName}/${folderName}/${fileName}`); @@ -289,11 +260,9 @@ export class FileController extends Controller { @Path() fileName: string, ) { const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", query: { - match: { - pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, - }, + match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}` }, }, }); @@ -318,3 +287,26 @@ export class FileController extends Controller { }; } } + +async function popInfo(pathname: string) { + const result = await esClient + .search }>({ + index: DEFAULT_INDEX!, + query: { match: { pathname } }, + }) + .catch((e) => console.error(e)); + + // pathname is unique and should not have multiple record with same path + if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) { + await esClient + .delete({ + index: DEFAULT_INDEX!, + id: result.hits.hits[0]._id, + }) + .catch((e) => console.error(e)); + + return result.hits.hits[0]._source; + } + + return false; +} diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index f838b6c..80251a6 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -53,7 +53,7 @@ export class FolderController extends Controller { @Post("/") @Tags("Folder") @Security("bearerAuth", ["admin"]) - @Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม") + @Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม") @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createFolder( @@ -72,7 +72,7 @@ export class FolderController extends Controller { }), ) ) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบแฟ้ม"); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม"); } const created = await minioClient From ece51deb296c0b6216c98e88e38bedb82f431193 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:04:39 +0700 Subject: [PATCH 12/29] chore: small text adjustment --- Services/server/src/rabbitmq/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/server/src/rabbitmq/handler.ts b/Services/server/src/rabbitmq/handler.ts index 2a74aa6..a9a4b13 100644 --- a/Services/server/src/rabbitmq/handler.ts +++ b/Services/server/src/rabbitmq/handler.ts @@ -99,7 +99,7 @@ async function handleNotFoundRecord( const metadata = { pathname, - fileName: filename ?? "n/a", // should not possible to fallback, just in case. + fileName: filename ?? "n/a", // should not possible to fallback, but just in case. fileSize: stat.size, fileType: stat.type, title: "", From 732a4b3989e7e08df2ad8f8bb2151d517b8726a6 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:04:59 +0700 Subject: [PATCH 13/29] chore: generate routes and swagger --- Services/server/src/routes.ts | 6 +++--- Services/server/src/swagger.json | 30 +++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index 689bd83..35ed08e 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -291,7 +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.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.uploadFile)), @@ -348,7 +348,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.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.updateFile)), @@ -379,7 +379,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":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.deleteFile)), diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index 6b57bcb..a997829 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -539,7 +539,7 @@ "operationId": "UploadFile", "responses": { "201": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -607,6 +607,9 @@ } } } + }, + "404": { + "description": "ไม่พบลิ้นชัก" } }, "tags": [ @@ -614,7 +617,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -682,7 +687,7 @@ "operationId": "GetFile", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -732,7 +737,7 @@ "operationId": "UpdateFile", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -753,6 +758,9 @@ } } } + }, + "404": { + "description": "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม" } }, "tags": [ @@ -760,7 +768,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -829,7 +839,7 @@ "operationId": "DeleteFile", "responses": { "200": { - "description": "" + "description": "สำเร็จ" } }, "tags": [ @@ -837,7 +847,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -973,7 +985,7 @@ } }, "tags": [ - "File" + "Download" ], "security": [], "parameters": [ @@ -1067,7 +1079,7 @@ "description": "สำเร็จ" }, "404": { - "description": "ไม่พบของแฟ้ม" + "description": "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม" }, "500": { "description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์" From 2c4d3846f1855631746d6cc7a426d2d9ea503ccd Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:41:46 +0700 Subject: [PATCH 14/29] refactor: rabbitmq file upload process --- .../server/src/controllers/fileController.ts | 162 ++++---- .../controllers/subFolderFileController.ts | 360 ++++++++---------- Services/server/src/routes.ts | 25 +- Services/server/src/swagger.json | 278 ++++++++------ 4 files changed, 398 insertions(+), 427 deletions(-) diff --git a/Services/server/src/controllers/fileController.ts b/Services/server/src/controllers/fileController.ts index 310a008..ec8b6a2 100644 --- a/Services/server/src/controllers/fileController.ts +++ b/Services/server/src/controllers/fileController.ts @@ -7,6 +7,7 @@ import { Path, Post, Request, + Response, Route, Security, SuccessResponse, @@ -32,8 +33,12 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." export class FileController extends Controller { @Post("/") @Tags("File") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.CREATED) + @Security("bearerAuth", ["admin"]) + @Response( + HttpStatusCode.NOT_FOUND, + "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ", + ) + @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async uploadFile( @Request() request: { user: { preferred_username: string } }, @Body() @@ -56,7 +61,25 @@ export class FileController extends Controller { "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ", ); } - const rec = await popInfo(pathname); + + const result = await esClient + .search }>({ + index: DEFAULT_INDEX!, + query: { match: { pathname } }, + }) + .catch((e) => console.error(e)); + + // pathname is unique and should not have multiple record with same path + if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) { + await esClient + .delete({ + index: DEFAULT_INDEX!, + id: result.hits.hits[0]._id, + }) + .catch((e) => console.error(e)); + } + + const rec = result ? result.hits.hits[0]._source : false; const metadata: Partial = { pathname, @@ -75,7 +98,7 @@ export class FileController extends Controller { }; await esClient.index({ - index: "dev-index", + index: DEFAULT_INDEX!, document: metadata, }); @@ -85,24 +108,21 @@ export class FileController extends Controller { createdBy: metadata.createdBy, updatedAt: metadata.updatedAt, updatedBy: metadata.updatedBy, - upload: await minioClient.presignedPutObject("ehr", pathname), + upload: await minioClient.presignedPutObject(DEFAULT_BUCKET!, pathname), }; } @Get("/") @Tags("File") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async getFile( @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, ): Promise { - const search = await esClient.search< - EhrFile & { - attachment: Record; - } - >({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + const search = await esClient.search }>({ + index: DEFAULT_INDEX!, query: { prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/`, @@ -124,8 +144,9 @@ export class FileController extends Controller { @Patch("/{fileName}") @Tags("File") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async updateFile( @Request() request: { user: { preferred_username: string } }, @Path() cabinetName: string, @@ -141,26 +162,30 @@ export class FileController extends Controller { keyword?: string; }, ): Promise { - const pathname = `${cabinetName}/${drawerName}/${folderName}/${fileName}`; + const basePath = `${cabinetName}/${drawerName}/${folderName}/`; + const pathname = `${basePath}${fileName}`; - if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/`))) { - throw new HttpError( - HttpStatusCode.PRECONDITION_FAILED, - "Cabinet, drawer or folder cannot be found.", - ); + if ( + !Boolean( + await minioClient.statObject(DEFAULT_BUCKET!, `${pathname}`).catch((e) => { + if (e.code === "NotFound") return false; + throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ) + ) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์"); } - // assume user will replace file by re-upload + // assume user will probably replace file by re-upload but maybe just rename if (body.file) { - const destination = `${cabinetName}/${drawerName}/${folderName}/${body.file}`; - const source = `ehr/${cabinetName}/${drawerName}/${folderName}/${fileName}`; - - const copy = await minioClient.copyObject("ehr", destination, source, copyCond); + const destination = `${basePath}${body.file}`; + const source = `/${DEFAULT_BUCKET}/${basePath}${fileName}`; + const copy = await minioClient.copyObject(DEFAULT_BUCKET!, destination, source, copyCond); if (copy) { const search = await esClient .search }>({ - index: "my-test-index", + index: DEFAULT_INDEX!, query: { match: { pathname } }, }) .catch((e) => console.error(e)); @@ -177,15 +202,15 @@ export class FileController extends Controller { updatedBy: request.user.preferred_username ?? "n/a", }, }) - .then(() => minioClient.removeObject("ehr", pathname)); + .then(() => minioClient.removeObject(DEFAULT_BUCKET!, pathname)); } else { - await minioClient.removeObject("ehr", pathname); + await minioClient.removeObject(DEFAULT_BUCKET!, pathname); } } } else { const search = await esClient .search }>({ - index: "my-test-index", + index: DEFAULT_INDEX!, query: { match: { pathname } }, }) .catch((e) => console.error(e)); @@ -207,52 +232,36 @@ export class FileController extends Controller { } return body.file - ? this.setStatus(HttpStatusCode.NO_CONTENT) - : { + ? { upload: await minioClient.presignedPutObject( - "ehr", - `${cabinetName}/${drawerName}/${folderName}/${body.file ?? fileName}`, + DEFAULT_BUCKET!, + `${basePath}${body.file ?? fileName}`, ), - }; + } + : this.setStatus(HttpStatusCode.NO_CONTENT); } @Delete("/{fileName}") @Tags("File") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth", ["admin"]) + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async deleteFile( @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @Path() fileName: string, ) { - const result = await esClient - .deleteByQuery({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - match: { - pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, - }, - }, - }) - .catch((e) => console.error(e)); - - if (result && result.total === 0) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "Data not found"); - } - - if (!result) { - throw new Error("An error occured, cannot perform this action."); - } - - await minioClient.removeObject("ehr", `${cabinetName}/${drawerName}/${folderName}/${fileName}`); - + await minioClient.removeObject( + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}/${fileName}`, + ); return this.setStatus(HttpStatusCode.NO_CONTENT); } @Get("/{fileName}") - @Tags("File") - @SuccessResponse(HttpStatusCode.OK) + @Tags("Download") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async downloadFile( @Path() cabinetName: string, @Path() drawerName: string, @@ -260,7 +269,7 @@ export class FileController extends Controller { @Path() fileName: string, ) { const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + index: DEFAULT_INDEX!, query: { match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}` }, }, @@ -270,43 +279,14 @@ export class FileController extends Controller { 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; + const { attachment, ...rest } = search.hits.hits[0]._source!; return { ...rest, download: await minioClient.presignedGetObject( - "ehr", + DEFAULT_BUCKET!, `${cabinetName}/${drawerName}/${folderName}/${fileName}`, ), }; } } - -async function popInfo(pathname: string) { - const result = await esClient - .search }>({ - index: DEFAULT_INDEX!, - query: { match: { pathname } }, - }) - .catch((e) => console.error(e)); - - // pathname is unique and should not have multiple record with same path - if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) { - await esClient - .delete({ - index: DEFAULT_INDEX!, - id: result.hits.hits[0]._id, - }) - .catch((e) => console.error(e)); - - return result.hits.hits[0]._source; - } - - return false; -} diff --git a/Services/server/src/controllers/subFolderFileController.ts b/Services/server/src/controllers/subFolderFileController.ts index af513d9..5fd993e 100644 --- a/Services/server/src/controllers/subFolderFileController.ts +++ b/Services/server/src/controllers/subFolderFileController.ts @@ -1,24 +1,33 @@ import { + Body, Controller, Delete, - FormField, Get, Patch, Path, Post, Request, + Response, Route, Security, SuccessResponse, Tags, - UploadedFile, } from "tsoa"; + import esClient from "../elasticsearch"; -import minioClient from "../storage"; +import minioClient from "../minio"; + import HttpStatusCode from "../interfaces/http-status"; -import { pathExist } from "../utils/minio"; -import HttpError from "../interfaces/http-error"; import { EhrFile } from "../interfaces/ehr-fs"; +import HttpError from "../interfaces/http-error"; + +import { copyCond, pathExist } from "../utils/minio"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); @Route( "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}/file", @@ -26,109 +35,98 @@ import { EhrFile } from "../interfaces/ehr-fs"; export class SubFolderFileController extends Controller { @Post("/") @Tags("SubFolder File") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.CREATED) + @Security("bearerAuth", ["admin"]) + @Response( + HttpStatusCode.NOT_FOUND, + "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ", + ) + @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, + @Body() + body: { + file: string; + title: string; + description: string; + category: string; + keyword: 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}`; + const pathname = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${body.file}`; if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/${subFolderName}`))) { throw new HttpError( - HttpStatusCode.PRECONDITION_FAILED, - "Cabinet, drawer, folder or subfolder cannot be found.", + HttpStatusCode.NOT_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, + const result = await esClient + .search }>({ + index: DEFAULT_INDEX!, + query: { match: { pathname } }, }) .catch((e) => console.error(e)); - if (!info) throw new Error("Object storage error occured."); + // pathname is unique and should not have multiple record with same path + if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) { + await esClient + .delete({ + index: DEFAULT_INDEX!, + id: result.hits.hits[0]._id, + }) + .catch((e) => console.error(e)); + } - const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - query: { - match: { - pathname: pathname, - }, - }, - }); - - const exist = search.hits.hits.find((v) => v._source?.pathname === pathname); + const rec = result ? result.hits.hits[0]._source : false; const metadata: Partial = { pathname, - fileName: filename, - fileSize: file.size, - fileType: file.mimetype, - title: title, - description: description, - category: category.split(","), - keyword: keyword.split(","), + fileName: body.file, + fileSize: 0, + fileType: "", + title: body.title, + description: body.description, + category: body.category.split(","), + keyword: body.keyword.split(","), + upload: false, + createdAt: new Date().toISOString(), + createdBy: rec ? rec.createdBy : "n/a", + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username ?? "n/a", }; - if (!exist) { - await esClient.index({ - pipeline: "attachment", - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - 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: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - 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, - }, - }); - } + await esClient.index({ + index: DEFAULT_INDEX!, + document: metadata, + }); - return this.setStatus(HttpStatusCode.CREATED); + return { + ...body, + createdAt: metadata.createdAt, + createdBy: metadata.createdBy, + updatedAt: metadata.updatedAt, + updatedBy: metadata.updatedBy, + upload: await minioClient.presignedPutObject(DEFAULT_BUCKET!, pathname), + }; } @Get("/") @Tags("SubFolder File") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @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: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', + ): Promise { + const search = await esClient.search }>({ + index: DEFAULT_INDEX!, query: { prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, @@ -136,24 +134,23 @@ export class SubFolderFileController 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) => { - if (!v._source) return; - - const { attachment, ...rest } = v._source; - - return rest; + if (v._source) { + const { attachment, ...rest } = v._source; + return rest satisfies EhrFile; + } }) - .flatMap((v) => (v ? [v] : [])); + .filter((v: EhrFile | undefined): v is EhrFile => !!v); return records; } @Patch("/{fileName}") @Tags("SubFolder File") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async updateFile( @Request() request: { user: { preferred_username: string } }, @Path() cabinetName: string, @@ -161,92 +158,98 @@ export class SubFolderFileController extends Controller { @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, + @Body() + body: { + file?: string; + title?: string; + description?: string; + category?: string; + keyword?: string; + }, ) { - const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - query: { - match: { - pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`, - }, - }, - }); + const basePath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`; + const pathname = `${basePath}${fileName}`; - if (search && search.hits.hits.length === 0) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); + if ( + !Boolean( + await minioClient.statObject(DEFAULT_BUCKET!, `${pathname}`).catch((e) => { + if (e.code === "NotFound") return false; + throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ) + ) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์"); } - const data = search.hits.hits[0]; + // assume user will probably replace file by re-upload but maybe just rename + if (body.file) { + const destination = `${basePath}${body.file}`; + const source = `/${DEFAULT_BUCKET}/${basePath}${fileName}`; + const copy = await minioClient.copyObject(DEFAULT_BUCKET!, destination, source, copyCond); - if (!file) { - const esResult = await esClient - .update({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - 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 (copy) { + const search = await esClient + .search }>({ + index: DEFAULT_INDEX!, + query: { match: { pathname } }, + }) + .catch((e) => console.error(e)); - if (!esResult) throw new Error("An error occured, cannot perform this action."); + if (search && search.hits.hits.length > 0 && search.hits.hits[0]._source) { + const { _index: index, _id: id } = search.hits.hits[0]; + await esClient + .update({ + index, + id, + doc: { + pathname: destination, + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username ?? "n/a", + }, + }) + .then(() => minioClient.removeObject(DEFAULT_BUCKET!, pathname)); + } else { + await minioClient.removeObject(DEFAULT_BUCKET!, pathname); + } + } } 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, + const search = await esClient + .search }>({ + index: DEFAULT_INDEX!, + query: { match: { pathname } }, }) .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: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - 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, - }, - }); + if (search && search.hits.hits.length > 0 && search.hits.hits[0]._source) { + const { _index: index, _id: id } = search.hits.hits[0]; + await esClient.update({ + index, + id, + doc: { + ...body, + keyword: body.keyword?.split(","), + category: body.category?.split(","), + updatedAt: new Date().toISOString(), + updatedBy: request.user.preferred_username ?? "n/a", + }, + }); + } } - return this.setStatus(HttpStatusCode.NO_CONTENT); + return body.file + ? { + upload: await minioClient.presignedPutObject( + DEFAULT_BUCKET!, + `${basePath}${body.file ?? fileName}`, + ), + } + : this.setStatus(HttpStatusCode.NO_CONTENT); } @Delete("/{fileName}") @Tags("SubFolder File") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth", ["admin"]) + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async deleteFile( @Path() cabinetName: string, @Path() drawerName: string, @@ -254,42 +257,15 @@ export class SubFolderFileController extends Controller { @Path() subFolderName: string, @Path() fileName: string, ) { - const search = await esClient.search< - EhrFile & { - attachment: Record; - } - >({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - 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: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', - 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}`, + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}/${fileName}/${subFolderName}/`, ); - return this.setStatus(HttpStatusCode.NO_CONTENT); } @Get("/{fileName}") - @Tags("File") + @Tags("Download") @SuccessResponse(HttpStatusCode.OK) public async downloadFile( @Path() cabinetName: string, @@ -299,7 +275,7 @@ export class SubFolderFileController extends Controller { @Path() fileName: string, ) { const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', + index: DEFAULT_INDEX!, query: { match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`, @@ -311,19 +287,13 @@ export class SubFolderFileController extends Controller { 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; + const { attachment, ...rest } = search.hits.hits[0]._source!; return { ...rest, download: await minioClient.presignedGetObject( - "ehr", - `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`, + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}/${fileName}`, ), }; } diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index 35ed08e..8f68008 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -20,8 +20,6 @@ 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 @@ -321,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.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.getFile)), @@ -408,6 +407,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', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.downloadFile)), @@ -691,19 +691,14 @@ 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'), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(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"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string","required":true},"category":{"dataType":"string","required":true},"description":{"dataType":"string","required":true},"title":{"dataType":"string","required":true},"file":{"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"}, @@ -727,6 +722,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/:subFolderName/file', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SubFolderFileController)), ...(fetchMiddlewares(SubFolderFileController.prototype.getFile)), @@ -755,8 +751,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.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file/:fileName', - authenticateMiddleware([{"bearerAuth":[]}]), - upload.single('file'), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(SubFolderFileController)), ...(fetchMiddlewares(SubFolderFileController.prototype.updateFile)), @@ -768,11 +763,7 @@ export function RegisterRoutes(app: Router) { 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"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string"},"category":{"dataType":"string"},"description":{"dataType":"string"},"title":{"dataType":"string"},"file":{"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 @@ -792,7 +783,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/file/:fileName', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(SubFolderFileController)), ...(fetchMiddlewares(SubFolderFileController.prototype.deleteFile)), diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index a997829..d8bcdc9 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -609,7 +609,7 @@ } }, "404": { - "description": "ไม่พบลิ้นชัก" + "description": "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ" } }, "tags": [ @@ -703,7 +703,11 @@ "tags": [ "File" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -891,7 +895,7 @@ "operationId": "DownloadFile", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -987,7 +991,11 @@ "tags": [ "Download" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -1536,7 +1544,77 @@ "operationId": "UploadFile", "responses": { "201": { - "description": "" + "description": "สำเร็จ", + "content": { + "application/json": { + "schema": { + "properties": { + "keyword": { + "type": "string" + }, + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "file": { + "type": "string" + }, + "upload": { + "type": "string" + }, + "updatedBy": { + "type": "string" + }, + "updatedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "createdBy": { + "type": "string" + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + } + }, + "required": [ + "keyword", + "category", + "description", + "title", + "file", + "upload", + "updatedBy", + "updatedAt", + "createdBy", + "createdAt" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ" } }, "tags": [ @@ -1544,7 +1622,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1584,34 +1664,33 @@ "requestBody": { "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { - "type": "object", "properties": { - "file": { - "type": "string", - "format": "binary" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, "keyword": { "type": "string" }, "category": { "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "file": { + "type": "string" } }, "required": [ - "file", - "title", - "description", "keyword", - "category" - ] + "category", + "description", + "title", + "file" + ], + "type": "object" } } } @@ -1621,91 +1700,12 @@ "operationId": "GetFile", "responses": { "200": { - "description": "", + "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" - } - ] - }, - "upload": { - "type": "boolean" - }, - "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", - "upload", - "keyword", - "category", - "description", - "title", - "fileType", - "fileSize", - "fileName", - "pathname" - ], - "type": "object" + "$ref": "#/components/schemas/EhrFile" }, "type": "array" } @@ -1716,7 +1716,11 @@ "tags": [ "SubFolder File" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -1758,7 +1762,30 @@ "operationId": "UpdateFile", "responses": { "200": { - "description": "" + "description": "สำเร็จ", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "properties": { + "upload": { + "type": "string" + } + }, + "required": [ + "upload" + ], + "type": "object" + } + ] + } + } + } + }, + "404": { + "description": "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม" } }, "tags": [ @@ -1766,7 +1793,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1812,29 +1841,28 @@ } ], "requestBody": { - "required": false, + "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { - "type": "object", "properties": { - "file": { - "type": "string", - "format": "binary" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, "keyword": { "type": "string" }, "category": { "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "file": { + "type": "string" } - } + }, + "type": "object" } } } @@ -1844,7 +1872,7 @@ "operationId": "DeleteFile", "responses": { "200": { - "description": "" + "description": "สำเร็จ" } }, "tags": [ @@ -1852,7 +1880,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1996,7 +2026,7 @@ } }, "tags": [ - "File" + "Download" ], "security": [], "parameters": [ From eed1a863d37d402851cf3489803f068f91e1d883 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:45:20 +0700 Subject: [PATCH 15/29] refactor: .env compat --- Services/server/src/controllers/searchController.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Services/server/src/controllers/searchController.ts b/Services/server/src/controllers/searchController.ts index 99de738..dea0d8e 100644 --- a/Services/server/src/controllers/searchController.ts +++ b/Services/server/src/controllers/searchController.ts @@ -1,23 +1,29 @@ -import { Body, Controller, Post, Route, SuccessResponse, Tags } from "tsoa"; +import { Body, Controller, Post, Route, Security, 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"; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); + @Route("/search") export class SearchController extends Controller { @Post("/") @Tags("Search") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async searchFile(@Body() search: Search): Promise { const result = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + index: DEFAULT_INDEX, query: { bool: { must: search.AND?.map((v) => ({ match: { [v.field]: v.value } })), should: search.OR?.map((v) => ({ match: { [v.field]: v.value } })), }, }, + size: 10000, }); return result.hits.hits.length > 0 From 9b2bff6ba58bf84c5435ab99794c74df297e824b Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:46:00 +0700 Subject: [PATCH 16/29] refactor: max number of record that return --- Services/server/src/controllers/fileController.ts | 1 + .../server/src/controllers/subFolderFileController.ts | 1 + Services/server/src/routes.ts | 1 + Services/server/src/swagger.json | 8 ++++++-- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Services/server/src/controllers/fileController.ts b/Services/server/src/controllers/fileController.ts index ec8b6a2..a728c9d 100644 --- a/Services/server/src/controllers/fileController.ts +++ b/Services/server/src/controllers/fileController.ts @@ -128,6 +128,7 @@ export class FileController extends Controller { pathname: `${cabinetName}/${drawerName}/${folderName}/`, }, }, + size: 10000, }); const records = search.hits.hits diff --git a/Services/server/src/controllers/subFolderFileController.ts b/Services/server/src/controllers/subFolderFileController.ts index 5fd993e..c5eb49a 100644 --- a/Services/server/src/controllers/subFolderFileController.ts +++ b/Services/server/src/controllers/subFolderFileController.ts @@ -132,6 +132,7 @@ export class SubFolderFileController extends Controller { pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, }, }, + size: 10000, }); const records = search.hits.hits diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index 8f68008..ba1a8e4 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -549,6 +549,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.post('/search', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SearchController)), ...(fetchMiddlewares(SearchController.prototype.searchFile)), diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index d8bcdc9..5d65870 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -1257,7 +1257,7 @@ "operationId": "SearchFile", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -1273,7 +1273,11 @@ "tags": [ "Search" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [], "requestBody": { "required": true, From d8cf5682068ed49c209631d3b5910e2a611e17d6 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:06:51 +0700 Subject: [PATCH 17/29] docs: word and order --- .../src/controllers/cabinetController.ts | 8 +- .../src/controllers/drawerController.ts | 8 +- .../server/src/controllers/fileController.ts | 70 ++-- .../src/controllers/folderController.ts | 8 +- .../src/controllers/searchController.ts | 2 +- .../src/controllers/subFolderController.ts | 8 +- .../controllers/subFolderFileController.ts | 73 ++--- Services/server/src/routes.ts | 115 +++---- Services/server/src/swagger.json | 303 ++++++++++-------- Services/server/tsoa.json | 11 +- 10 files changed, 322 insertions(+), 284 deletions(-) diff --git a/Services/server/src/controllers/cabinetController.ts b/Services/server/src/controllers/cabinetController.ts index fadac3d..32d0b68 100644 --- a/Services/server/src/controllers/cabinetController.ts +++ b/Services/server/src/controllers/cabinetController.ts @@ -31,7 +31,7 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." @Route("cabinet") export class CabinetController extends Controller { @Get("/") - @Tags("Cabinet") + @Tags("ตู้เอกสาร") @Security("bearerAuth") @Response( HttpStatusCode.INTERNAL_SERVER_ERROR, @@ -48,7 +48,7 @@ export class CabinetController extends Controller { } @Post("/") - @Tags("Cabinet") + @Tags("ตู้เอกสาร") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") @@ -69,7 +69,7 @@ export class CabinetController extends Controller { } @Put("/{cabinetName}") - @Tags("Cabinet") + @Tags("ตู้เอกสาร") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") @@ -122,7 +122,7 @@ export class CabinetController extends Controller { } @Delete("/{cabinetName}") - @Tags("Cabinet") + @Tags("ตู้เอกสาร") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้") @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") diff --git a/Services/server/src/controllers/drawerController.ts b/Services/server/src/controllers/drawerController.ts index 329a1b4..00ef246 100644 --- a/Services/server/src/controllers/drawerController.ts +++ b/Services/server/src/controllers/drawerController.ts @@ -32,7 +32,7 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." @Route("/cabinet/{cabinetName}/drawer") export class DrawerController extends Controller { @Get("/") - @Tags("Drawer") + @Tags("ลิ้นชัก") @Security("bearerAuth") @Response( HttpStatusCode.INTERNAL_SERVER_ERROR, @@ -49,7 +49,7 @@ export class DrawerController extends Controller { } @Post("/") - @Tags("Drawer") + @Tags("ลิ้นชัก") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.NOT_FOUND, "ไม่พบลิ้นชัก") @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") @@ -85,7 +85,7 @@ export class DrawerController extends Controller { } @Put("/{drawerName}") - @Tags("Drawer") + @Tags("ลิ้นชัก") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") @@ -141,7 +141,7 @@ export class DrawerController extends Controller { } @Delete("/{drawerName}") - @Tags("Drawer") + @Tags("ลิ้นชัก") @Security("bearerAuth", ["admin"]) @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) { diff --git a/Services/server/src/controllers/fileController.ts b/Services/server/src/controllers/fileController.ts index a728c9d..28fb0e9 100644 --- a/Services/server/src/controllers/fileController.ts +++ b/Services/server/src/controllers/fileController.ts @@ -31,8 +31,39 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file") export class FileController extends Controller { + @Get("/") + @Tags("ไฟล์") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") + public async getFile( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + ): Promise { + const search = await esClient.search }>({ + index: DEFAULT_INDEX!, + query: { + prefix: { + pathname: `${cabinetName}/${drawerName}/${folderName}/`, + }, + }, + size: 10000, + }); + + const records = search.hits.hits + .map((v) => { + if (v._source) { + const { attachment, ...rest } = v._source; + return rest satisfies EhrFile; + } + }) + .filter((v: EhrFile | undefined): v is EhrFile => !!v); + + return records; + } + @Post("/") - @Tags("File") + @Tags("ไฟล์") @Security("bearerAuth", ["admin"]) @Response( HttpStatusCode.NOT_FOUND, @@ -112,39 +143,8 @@ export class FileController extends Controller { }; } - @Get("/") - @Tags("File") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") - public async getFile( - @Path() cabinetName: string, - @Path() drawerName: string, - @Path() folderName: string, - ): Promise { - const search = await esClient.search }>({ - index: DEFAULT_INDEX!, - query: { - prefix: { - pathname: `${cabinetName}/${drawerName}/${folderName}/`, - }, - }, - size: 10000, - }); - - const records = search.hits.hits - .map((v) => { - if (v._source) { - const { attachment, ...rest } = v._source; - return rest satisfies EhrFile; - } - }) - .filter((v: EhrFile | undefined): v is EhrFile => !!v); - - return records; - } - @Patch("/{fileName}") - @Tags("File") + @Tags("ไฟล์") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม") @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") @@ -243,7 +243,7 @@ export class FileController extends Controller { } @Delete("/{fileName}") - @Tags("File") + @Tags("ไฟล์") @Security("bearerAuth", ["admin"]) @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async deleteFile( @@ -260,7 +260,7 @@ export class FileController extends Controller { } @Get("/{fileName}") - @Tags("Download") + @Tags("ดาวน์โหลด") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async downloadFile( diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index 80251a6..f872ffe 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -32,7 +32,7 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder") export class FolderController extends Controller { @Get("/") - @Tags("Folder") + @Tags("แฟ้ม") @Security("bearerAuth") @Response( HttpStatusCode.INTERNAL_SERVER_ERROR, @@ -51,7 +51,7 @@ export class FolderController extends Controller { } @Post("/") - @Tags("Folder") + @Tags("แฟ้ม") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม") @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") @@ -88,7 +88,7 @@ export class FolderController extends Controller { } @Put("/{folderName}") - @Tags("Folder") + @Tags("แฟ้ม") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") @@ -145,7 +145,7 @@ export class FolderController extends Controller { } @Delete("/{folderName}") - @Tags("Folder") + @Tags("แฟ้ม") @Security("bearerAuth", ["admin"]) @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteFolder( diff --git a/Services/server/src/controllers/searchController.ts b/Services/server/src/controllers/searchController.ts index dea0d8e..af352bf 100644 --- a/Services/server/src/controllers/searchController.ts +++ b/Services/server/src/controllers/searchController.ts @@ -11,7 +11,7 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." @Route("/search") export class SearchController extends Controller { @Post("/") - @Tags("Search") + @Tags("ค้นหา") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async searchFile(@Body() search: Search): Promise { diff --git a/Services/server/src/controllers/subFolderController.ts b/Services/server/src/controllers/subFolderController.ts index 376bae8..426af62 100644 --- a/Services/server/src/controllers/subFolderController.ts +++ b/Services/server/src/controllers/subFolderController.ts @@ -32,7 +32,7 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") export class SubFolderController extends Controller { @Get("/") - @Tags("SubFolder") + @Tags("แฟ้มย่อย") @Security("bearerAuth") @Response( HttpStatusCode.INTERNAL_SERVER_ERROR, @@ -53,7 +53,7 @@ export class SubFolderController extends Controller { } @Post("/") - @Tags("SubFolder") + @Tags("แฟ้มย่อย") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม") @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") @@ -91,7 +91,7 @@ export class SubFolderController extends Controller { } @Put("/{subFolderName}") - @Tags("SubFolder") + @Tags("แฟ้มย่อย") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") @@ -149,7 +149,7 @@ export class SubFolderController extends Controller { } @Delete("/{subFolderName}") - @Tags("SubFolder") + @Tags("แฟ้มย่อย") @Security("bearerAuth", ["admin"]) @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteFolder( diff --git a/Services/server/src/controllers/subFolderFileController.ts b/Services/server/src/controllers/subFolderFileController.ts index c5eb49a..1a61cd5 100644 --- a/Services/server/src/controllers/subFolderFileController.ts +++ b/Services/server/src/controllers/subFolderFileController.ts @@ -33,8 +33,40 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}/file", ) export class SubFolderFileController extends Controller { + @Get("/") + @Tags("ไฟล์") + @Security("bearerAuth") + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") + public async getFile( + @Path() cabinetName: string, + @Path() drawerName: string, + @Path() folderName: string, + @Path() subFolderName: string, + ): Promise { + const search = await esClient.search }>({ + index: DEFAULT_INDEX!, + query: { + prefix: { + pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, + }, + }, + size: 10000, + }); + + const records = search.hits.hits + .map((v) => { + if (v._source) { + const { attachment, ...rest } = v._source; + return rest satisfies EhrFile; + } + }) + .filter((v: EhrFile | undefined): v is EhrFile => !!v); + + return records; + } + @Post("/") - @Tags("SubFolder File") + @Tags("ไฟล์") @Security("bearerAuth", ["admin"]) @Response( HttpStatusCode.NOT_FOUND, @@ -115,40 +147,8 @@ export class SubFolderFileController extends Controller { }; } - @Get("/") - @Tags("SubFolder File") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") - public async getFile( - @Path() cabinetName: string, - @Path() drawerName: string, - @Path() folderName: string, - @Path() subFolderName: string, - ): Promise { - const search = await esClient.search }>({ - index: DEFAULT_INDEX!, - query: { - prefix: { - pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, - }, - }, - size: 10000, - }); - - const records = search.hits.hits - .map((v) => { - if (v._source) { - const { attachment, ...rest } = v._source; - return rest satisfies EhrFile; - } - }) - .filter((v: EhrFile | undefined): v is EhrFile => !!v); - - return records; - } - @Patch("/{fileName}") - @Tags("SubFolder File") + @Tags("ไฟล์") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม") @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") @@ -248,7 +248,7 @@ export class SubFolderFileController extends Controller { } @Delete("/{fileName}") - @Tags("SubFolder File") + @Tags("ไฟล์") @Security("bearerAuth", ["admin"]) @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async deleteFile( @@ -266,7 +266,8 @@ export class SubFolderFileController extends Controller { } @Get("/{fileName}") - @Tags("Download") + @Tags("ดาวน์โหลด") + @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) public async downloadFile( @Path() cabinetName: string, diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index ba1a8e4..d84b67a 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -288,6 +288,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', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(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); + } + }); + // 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/file', authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(FileController)), @@ -318,34 +346,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)), - - 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); - } - }); - // 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":["admin"]}]), ...(fetchMiddlewares(FileController)), @@ -691,6 +691,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', + authenticateMiddleware([{"bearerAuth":[]}]), + ...(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.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file', authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(SubFolderFileController)), @@ -722,35 +751,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/:subFolderName/file', - authenticateMiddleware([{"bearerAuth":[]}]), - ...(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":["admin"]}]), ...(fetchMiddlewares(SubFolderFileController)), @@ -814,6 +814,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/:subFolderName/file/:fileName', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SubFolderFileController)), ...(fetchMiddlewares(SubFolderFileController.prototype.downloadFile)), diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index 5d65870..d511595 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -214,7 +214,7 @@ } }, "tags": [ - "Cabinet" + "ตู้เอกสาร" ], "security": [ { @@ -234,7 +234,7 @@ } }, "tags": [ - "Cabinet" + "ตู้เอกสาร" ], "security": [ { @@ -276,7 +276,7 @@ } }, "tags": [ - "Cabinet" + "ตู้เอกสาร" ], "security": [ { @@ -325,7 +325,7 @@ } }, "tags": [ - "Cabinet" + "ตู้เอกสาร" ], "security": [ { @@ -368,7 +368,7 @@ } }, "tags": [ - "Drawer" + "ลิ้นชัก" ], "security": [ { @@ -400,7 +400,7 @@ } }, "tags": [ - "Drawer" + "ลิ้นชัก" ], "security": [ { @@ -451,7 +451,7 @@ } }, "tags": [ - "Drawer" + "ลิ้นชัก" ], "security": [ { @@ -505,7 +505,7 @@ } }, "tags": [ - "Drawer" + "ลิ้นชัก" ], "security": [ { @@ -535,6 +535,58 @@ } }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file": { + "get": { + "operationId": "GetFile", + "responses": { + "200": { + "description": "สำเร็จ", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/EhrFile" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "ไฟล์" + ], + "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" + } + } + ] + }, "post": { "operationId": "UploadFile", "responses": { @@ -613,7 +665,7 @@ } }, "tags": [ - "File" + "ไฟล์" ], "security": [ { @@ -682,58 +734,6 @@ } } } - }, - "get": { - "operationId": "GetFile", - "responses": { - "200": { - "description": "สำเร็จ", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/EhrFile" - }, - "type": "array" - } - } - } - } - }, - "tags": [ - "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" - } - } - ] } }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file/{fileName}": { @@ -768,7 +768,7 @@ } }, "tags": [ - "File" + "ไฟล์" ], "security": [ { @@ -847,7 +847,7 @@ } }, "tags": [ - "File" + "ไฟล์" ], "security": [ { @@ -989,7 +989,7 @@ } }, "tags": [ - "Download" + "ดาวน์โหลด" ], "security": [ { @@ -1054,7 +1054,7 @@ } }, "tags": [ - "Folder" + "แฟ้ม" ], "security": [ { @@ -1094,7 +1094,7 @@ } }, "tags": [ - "Folder" + "แฟ้ม" ], "security": [ { @@ -1153,7 +1153,7 @@ } }, "tags": [ - "Folder" + "แฟ้ม" ], "security": [ { @@ -1215,7 +1215,7 @@ } }, "tags": [ - "Folder" + "แฟ้ม" ], "security": [ { @@ -1271,7 +1271,7 @@ } }, "tags": [ - "Search" + "ค้นหา" ], "security": [ { @@ -1313,7 +1313,7 @@ } }, "tags": [ - "SubFolder" + "แฟ้มย่อย" ], "security": [ { @@ -1361,7 +1361,7 @@ } }, "tags": [ - "SubFolder" + "แฟ้มย่อย" ], "security": [ { @@ -1428,7 +1428,7 @@ } }, "tags": [ - "SubFolder" + "แฟ้มย่อย" ], "security": [ { @@ -1498,7 +1498,7 @@ } }, "tags": [ - "SubFolder" + "แฟ้มย่อย" ], "security": [ { @@ -1544,6 +1544,66 @@ } }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}/file": { + "get": { + "operationId": "GetFile", + "responses": { + "200": { + "description": "สำเร็จ", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/EhrFile" + }, + "type": "array" + } + } + } + } + }, + "tags": [ + "ไฟล์" + ], + "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" + } + } + ] + }, "post": { "operationId": "UploadFile", "responses": { @@ -1622,7 +1682,7 @@ } }, "tags": [ - "SubFolder File" + "ไฟล์" ], "security": [ { @@ -1699,66 +1759,6 @@ } } } - }, - "get": { - "operationId": "GetFile", - "responses": { - "200": { - "description": "สำเร็จ", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/EhrFile" - }, - "type": "array" - } - } - } - } - }, - "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" - } - } - ] } }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder/{subFolderName}/file/{fileName}": { @@ -1793,7 +1793,7 @@ } }, "tags": [ - "SubFolder File" + "ไฟล์" ], "security": [ { @@ -1880,7 +1880,7 @@ } }, "tags": [ - "SubFolder File" + "ไฟล์" ], "security": [ { @@ -2030,9 +2030,13 @@ } }, "tags": [ - "Download" + "ดาวน์โหลด" + ], + "security": [ + { + "bearerAuth": [] + } ], - "security": [], "parameters": [ { "in": "path", @@ -2083,5 +2087,28 @@ "url": "/api" } ], + "tags": [ + { + "name": "ตู้เอกสาร" + }, + { + "name": "ลิ้นชัก" + }, + { + "name": "แฟ้ม" + }, + { + "name": "แฟ้มย่อย" + }, + { + "name": "ไฟล์" + }, + { + "name": "ดาวน์โหลด" + }, + { + "name": "ค้นหา" + } + ], "basePath": "/api" } \ No newline at end of file diff --git a/Services/server/tsoa.json b/Services/server/tsoa.json index e88a36b..24383ef 100644 --- a/Services/server/tsoa.json +++ b/Services/server/tsoa.json @@ -25,7 +25,16 @@ "description": "Keycloak Bearer Token", "in": "header" } - } + }, + "tags": [ + { "name": "ตู้เอกสาร" }, + { "name": "ลิ้นชัก" }, + { "name": "แฟ้ม" }, + { "name": "แฟ้มย่อย" }, + { "name": "ไฟล์" }, + { "name": "ดาวน์โหลด" }, + { "name": "ค้นหา" } + ] }, "routes": { "routesDir": "src", From 3820a05704d8a907e6b5be8f45dd26c2d5ac350e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:11:27 +0700 Subject: [PATCH 18/29] refactor: word --- .../src/controllers/cabinetController.ts | 6 +++--- .../src/controllers/drawerController.ts | 6 +++--- .../server/src/controllers/fileController.ts | 20 +++++++++---------- .../src/controllers/folderController.ts | 6 +++--- .../src/controllers/searchController.ts | 6 +++--- .../src/controllers/subFolderController.ts | 6 +++--- .../controllers/subFolderFileController.ts | 20 +++++++++---------- .../interfaces/{ehr-fs.ts => storage-fs.ts} | 4 ++-- Services/server/src/rabbitmq/handler.ts | 8 ++++---- Services/server/src/routes.ts | 4 ++-- Services/server/src/swagger.json | 18 ++++++++--------- Services/server/src/utils/minio.ts | 8 ++++---- 12 files changed, 56 insertions(+), 56 deletions(-) rename Services/server/src/interfaces/{ehr-fs.ts => storage-fs.ts} (91%) diff --git a/Services/server/src/controllers/cabinetController.ts b/Services/server/src/controllers/cabinetController.ts index 32d0b68..b900dbb 100644 --- a/Services/server/src/controllers/cabinetController.ts +++ b/Services/server/src/controllers/cabinetController.ts @@ -20,7 +20,7 @@ import esClient from "../elasticsearch"; import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; import HttpStatusCode from "../interfaces/http-status"; -import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; +import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; const DEFAULT_BUCKET = process.env.MINIO_BUCKET; const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; @@ -38,7 +38,7 @@ export class CabinetController extends Controller { "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง", ) @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") - public async listCabinet(): Promise { + public async listCabinet(): Promise { const list = await listFolder(DEFAULT_BUCKET!).catch((e) => console.error(`Error List Folder: ${e}`), ); @@ -94,7 +94,7 @@ export class CabinetController extends Controller { return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); } - const search = await esClient.search }>({ + const search = await esClient.search }>({ index: DEFAULT_INDEX!, query: { match: { pathname: current.name } }, }); diff --git a/Services/server/src/controllers/drawerController.ts b/Services/server/src/controllers/drawerController.ts index 00ef246..8e8df67 100644 --- a/Services/server/src/controllers/drawerController.ts +++ b/Services/server/src/controllers/drawerController.ts @@ -20,7 +20,7 @@ import esClient from "../elasticsearch"; import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; import HttpStatusCode from "../interfaces/http-status"; -import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; +import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; import HttpError from "../interfaces/http-error"; const DEFAULT_BUCKET = process.env.MINIO_BUCKET; @@ -39,7 +39,7 @@ export class DrawerController extends Controller { "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง", ) @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") - public async listDrawer(@Path() cabinetName: string): Promise { + public async listDrawer(@Path() cabinetName: string): Promise { const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/`).catch((e) => console.error(`Error List Folder: ${e}`), ); @@ -113,7 +113,7 @@ export class DrawerController extends Controller { return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); } - const search = await esClient.search }>({ + const search = await esClient.search }>({ index: DEFAULT_INDEX!, query: { match: { pathname: current.name } }, }); diff --git a/Services/server/src/controllers/fileController.ts b/Services/server/src/controllers/fileController.ts index 28fb0e9..b764926 100644 --- a/Services/server/src/controllers/fileController.ts +++ b/Services/server/src/controllers/fileController.ts @@ -18,7 +18,7 @@ import esClient from "../elasticsearch"; import minioClient from "../minio"; import HttpStatusCode from "../interfaces/http-status"; -import { EhrFile } from "../interfaces/ehr-fs"; +import { StorageFile } from "../interfaces/storage-fs"; import HttpError from "../interfaces/http-error"; import { copyCond, pathExist } from "../utils/minio"; @@ -39,8 +39,8 @@ export class FileController extends Controller { @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, - ): Promise { - const search = await esClient.search }>({ + ): Promise { + const search = await esClient.search }>({ index: DEFAULT_INDEX!, query: { prefix: { @@ -54,10 +54,10 @@ export class FileController extends Controller { .map((v) => { if (v._source) { const { attachment, ...rest } = v._source; - return rest satisfies EhrFile; + return rest satisfies StorageFile; } }) - .filter((v: EhrFile | undefined): v is EhrFile => !!v); + .filter((v: StorageFile | undefined): v is StorageFile => !!v); return records; } @@ -94,7 +94,7 @@ export class FileController extends Controller { } const result = await esClient - .search }>({ + .search }>({ index: DEFAULT_INDEX!, query: { match: { pathname } }, }) @@ -112,7 +112,7 @@ export class FileController extends Controller { const rec = result ? result.hits.hits[0]._source : false; - const metadata: Partial = { + const metadata: Partial = { pathname, fileName: body.file, fileSize: 0, @@ -185,7 +185,7 @@ export class FileController extends Controller { if (copy) { const search = await esClient - .search }>({ + .search }>({ index: DEFAULT_INDEX!, query: { match: { pathname } }, }) @@ -210,7 +210,7 @@ export class FileController extends Controller { } } else { const search = await esClient - .search }>({ + .search }>({ index: DEFAULT_INDEX!, query: { match: { pathname } }, }) @@ -269,7 +269,7 @@ export class FileController extends Controller { @Path() folderName: string, @Path() fileName: string, ) { - const search = await esClient.search }>({ + const search = await esClient.search }>({ index: DEFAULT_INDEX!, query: { match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}` }, diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index f872ffe..ebeef78 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -20,7 +20,7 @@ import esClient from "../elasticsearch"; import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; import HttpStatusCode from "../interfaces/http-status"; -import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; +import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; import HttpError from "../interfaces/http-error"; const DEFAULT_BUCKET = process.env.MINIO_BUCKET; @@ -42,7 +42,7 @@ export class FolderController extends Controller { public async listFolder( @Path() cabinetName: string, @Path() drawerName: string, - ): Promise { + ): Promise { const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/${drawerName}`).catch((e) => console.error(`Error List Folder: ${e}`), ); @@ -117,7 +117,7 @@ export class FolderController extends Controller { return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); } - const search = await esClient.search }>({ + const search = await esClient.search }>({ index: DEFAULT_INDEX!, query: { match: { pathname: current.name } }, }); diff --git a/Services/server/src/controllers/searchController.ts b/Services/server/src/controllers/searchController.ts index af352bf..0a6c669 100644 --- a/Services/server/src/controllers/searchController.ts +++ b/Services/server/src/controllers/searchController.ts @@ -2,7 +2,7 @@ import { Body, Controller, Post, Route, Security, SuccessResponse, Tags } from " import HttpStatusCode from "../interfaces/http-status"; import esClient from "../elasticsearch"; import { Search } from "../interfaces/search"; -import { EhrFile } from "../interfaces/ehr-fs"; +import { StorageFile } from "../interfaces/storage-fs"; const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; @@ -14,8 +14,8 @@ export class SearchController extends Controller { @Tags("ค้นหา") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") - public async searchFile(@Body() search: Search): Promise { - const result = await esClient.search }>({ + public async searchFile(@Body() search: Search): Promise { + const result = await esClient.search }>({ index: DEFAULT_INDEX, query: { bool: { diff --git a/Services/server/src/controllers/subFolderController.ts b/Services/server/src/controllers/subFolderController.ts index 426af62..43dc1fa 100644 --- a/Services/server/src/controllers/subFolderController.ts +++ b/Services/server/src/controllers/subFolderController.ts @@ -20,7 +20,7 @@ import esClient from "../elasticsearch"; import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; import HttpStatusCode from "../interfaces/http-status"; -import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; +import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; import HttpError from "../interfaces/http-error"; const DEFAULT_BUCKET = process.env.MINIO_BUCKET; @@ -43,7 +43,7 @@ export class SubFolderController extends Controller { @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, - ): Promise { + ): Promise { const list = await listFolder( DEFAULT_BUCKET!, `${cabinetName}/${drawerName}/${folderName}`, @@ -121,7 +121,7 @@ export class SubFolderController extends Controller { return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); } - const search = await esClient.search }>({ + const search = await esClient.search }>({ index: DEFAULT_INDEX!, query: { match: { pathname: current.name } }, }); diff --git a/Services/server/src/controllers/subFolderFileController.ts b/Services/server/src/controllers/subFolderFileController.ts index 1a61cd5..bebfb15 100644 --- a/Services/server/src/controllers/subFolderFileController.ts +++ b/Services/server/src/controllers/subFolderFileController.ts @@ -18,7 +18,7 @@ import esClient from "../elasticsearch"; import minioClient from "../minio"; import HttpStatusCode from "../interfaces/http-status"; -import { EhrFile } from "../interfaces/ehr-fs"; +import { StorageFile } from "../interfaces/storage-fs"; import HttpError from "../interfaces/http-error"; import { copyCond, pathExist } from "../utils/minio"; @@ -42,8 +42,8 @@ export class SubFolderFileController extends Controller { @Path() drawerName: string, @Path() folderName: string, @Path() subFolderName: string, - ): Promise { - const search = await esClient.search }>({ + ): Promise { + const search = await esClient.search }>({ index: DEFAULT_INDEX!, query: { prefix: { @@ -57,10 +57,10 @@ export class SubFolderFileController extends Controller { .map((v) => { if (v._source) { const { attachment, ...rest } = v._source; - return rest satisfies EhrFile; + return rest satisfies StorageFile; } }) - .filter((v: EhrFile | undefined): v is EhrFile => !!v); + .filter((v: StorageFile | undefined): v is StorageFile => !!v); return records; } @@ -98,7 +98,7 @@ export class SubFolderFileController extends Controller { } const result = await esClient - .search }>({ + .search }>({ index: DEFAULT_INDEX!, query: { match: { pathname } }, }) @@ -116,7 +116,7 @@ export class SubFolderFileController extends Controller { const rec = result ? result.hits.hits[0]._source : false; - const metadata: Partial = { + const metadata: Partial = { pathname, fileName: body.file, fileSize: 0, @@ -190,7 +190,7 @@ export class SubFolderFileController extends Controller { if (copy) { const search = await esClient - .search }>({ + .search }>({ index: DEFAULT_INDEX!, query: { match: { pathname } }, }) @@ -215,7 +215,7 @@ export class SubFolderFileController extends Controller { } } else { const search = await esClient - .search }>({ + .search }>({ index: DEFAULT_INDEX!, query: { match: { pathname } }, }) @@ -276,7 +276,7 @@ export class SubFolderFileController extends Controller { @Path() subFolderName: string, @Path() fileName: string, ) { - const search = await esClient.search }>({ + const search = await esClient.search }>({ index: DEFAULT_INDEX!, query: { match: { diff --git a/Services/server/src/interfaces/ehr-fs.ts b/Services/server/src/interfaces/storage-fs.ts similarity index 91% rename from Services/server/src/interfaces/ehr-fs.ts rename to Services/server/src/interfaces/storage-fs.ts index 77a3c9b..58737fd 100644 --- a/Services/server/src/interfaces/ehr-fs.ts +++ b/Services/server/src/interfaces/storage-fs.ts @@ -1,4 +1,4 @@ -export interface EhrFolder { +export interface StorageFolder { /** * @prop Full path to this folder. It is used as key as there are no files or directories at the same location. */ @@ -12,7 +12,7 @@ export interface EhrFolder { createdBy: string | Date; } -export interface EhrFile { +export interface StorageFile { /** * @prop Full path to this folder. It is used as key as there are no files or directories at the same location. */ diff --git a/Services/server/src/rabbitmq/handler.ts b/Services/server/src/rabbitmq/handler.ts index a9a4b13..82c51ae 100644 --- a/Services/server/src/rabbitmq/handler.ts +++ b/Services/server/src/rabbitmq/handler.ts @@ -1,4 +1,4 @@ -import { EhrFile } from "../interfaces/ehr-fs"; +import { StorageFile } from "../interfaces/storage-fs"; import esClient from "../elasticsearch"; import minioClient from "../minio"; @@ -48,7 +48,7 @@ export async function handler(key: string, event: string): Promise { // Get info and delete it from ElasticSearch to re-index async function popInfo(pathname: string) { const result = await esClient - .search }>({ + .search }>({ index: DEFAULT_INDEX!, query: { match: { pathname } }, }) @@ -111,7 +111,7 @@ async function handleNotFoundRecord( createdBy: "n/a", updatedAt: new Date().toISOString(), updatedBy: "n/a", - } satisfies Partial; + } satisfies Partial; const result = await esClient .index({ @@ -127,7 +127,7 @@ async function handleNotFoundRecord( } async function handleFoundRecord( - metadata: EhrFile, + metadata: StorageFile, buffer: Buffer, stat: { size: number; type: string }, ) { diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index d84b67a..396e16c 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -24,7 +24,7 @@ import type { RequestHandler, Router } from 'express'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const models: TsoaRoute.Models = { - "EhrFolder": { + "StorageFolder": { "dataType": "refObject", "properties": { "pathname": {"dataType":"string","required":true}, @@ -35,7 +35,7 @@ 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": { + "StorageFile": { "dataType": "refObject", "properties": { "pathname": {"dataType":"string","required":true}, diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index d511595..9c56b10 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -6,7 +6,7 @@ "requestBodies": {}, "responses": {}, "schemas": { - "EhrFolder": { + "StorageFolder": { "properties": { "pathname": { "type": "string" @@ -46,7 +46,7 @@ "type": "object", "additionalProperties": false }, - "EhrFile": { + "StorageFile": { "properties": { "pathname": { "type": "string" @@ -202,7 +202,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/EhrFolder" + "$ref": "#/components/schemas/StorageFolder" }, "type": "array" } @@ -356,7 +356,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/EhrFolder" + "$ref": "#/components/schemas/StorageFolder" }, "type": "array" } @@ -544,7 +544,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/EhrFile" + "$ref": "#/components/schemas/StorageFile" }, "type": "array" } @@ -1042,7 +1042,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/EhrFolder" + "$ref": "#/components/schemas/StorageFolder" }, "type": "array" } @@ -1262,7 +1262,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/EhrFile" + "$ref": "#/components/schemas/StorageFile" }, "type": "array" } @@ -1301,7 +1301,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/EhrFolder" + "$ref": "#/components/schemas/StorageFolder" }, "type": "array" } @@ -1553,7 +1553,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/EhrFile" + "$ref": "#/components/schemas/StorageFile" }, "type": "array" } diff --git a/Services/server/src/utils/minio.ts b/Services/server/src/utils/minio.ts index 53cf2b2..d3d9088 100644 --- a/Services/server/src/utils/minio.ts +++ b/Services/server/src/utils/minio.ts @@ -1,4 +1,4 @@ -import { EhrFolder } from "../interfaces/ehr-fs"; +import { StorageFolder } from "../interfaces/storage-fs"; import * as Minio from "minio"; import minioClient from "../minio"; @@ -29,7 +29,7 @@ export async function pathExist(path: string): Promise { * @param path - path to list * @return list of folder with metadata */ -export async function listFolder(bucket: string, path?: string): Promise { +export async function listFolder(bucket: string, path?: string): Promise { if (path) path = `${path.replace(/^\/|\/$/g, "")}/`; const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => { @@ -63,11 +63,11 @@ export async function listFolder(bucket: string, path?: string): Promise !!v); + return folder.filter((v: (typeof folder)[number]): v is StorageFolder => !!v); } export async function listItem( From 75025ca596b706ec4331371ecbe1eb825819ec48 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:32:08 +0700 Subject: [PATCH 19/29] docs: cabinet --- .../src/controllers/cabinetController.ts | 41 ++++++++++++++++-- Services/server/src/swagger.json | 42 +++++++++++++++---- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/Services/server/src/controllers/cabinetController.ts b/Services/server/src/controllers/cabinetController.ts index b900dbb..3c3bf10 100644 --- a/Services/server/src/controllers/cabinetController.ts +++ b/Services/server/src/controllers/cabinetController.ts @@ -12,6 +12,7 @@ import { Tags, Request, Response, + Example, } from "tsoa"; import minioClient from "../minio"; @@ -38,6 +39,20 @@ export class CabinetController extends Controller { "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง", ) @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") + @Example([ + { + path: "ตู้เอกสาร 1/", + name: "ตู้เอกสาร 1", + createdAt: "2021-07-20T12:33:13.018Z", + createdBy: "admin", + }, + { + path: "ตู้เอกสาร 2/", + name: "ตู้เอกสาร 2", + createdAt: "2022-01-23T16:05:02.114Z", + createdBy: "admin", + }, + ]) public async listCabinet(): Promise { const list = await listFolder(DEFAULT_BUCKET!).catch((e) => console.error(`Error List Folder: ${e}`), @@ -54,7 +69,13 @@ export class CabinetController extends Controller { @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createCabinet( @Request() request: { user: { preferred_username: string } }, - @Body() body: { name: string }, + @Body() + body: { + /** + * @example "ตู้เอกสาร 1" + */ + name: string; + }, ) { const created = await minioClient .putObject(DEFAULT_BUCKET!, `${replaceIllegalChars(body.name)}/.keep`, "", 0, { @@ -68,6 +89,9 @@ export class CabinetController extends Controller { return this.setStatus(HttpStatusCode.CREATED); } + /** + * @example cabinetName "ตู้เอกสาร 1" + */ @Put("/{cabinetName}") @Tags("ตู้เอกสาร") @Security("bearerAuth", ["admin"]) @@ -75,7 +99,13 @@ export class CabinetController extends Controller { @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editCabinet( @Path() cabinetName: string, - @Body() body: { name: string }, + @Body() + body: { + /** + * @example "ตู้เอกสารใหม่" + */ + name: string; + }, ): Promise { const path = `${cabinetName}/`; const list = await listItem(DEFAULT_BUCKET!, path, true); @@ -94,7 +124,9 @@ export class CabinetController extends Controller { return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); } - const search = await esClient.search }>({ + const search = await esClient.search< + StorageFile & { attachment: Record } + >({ index: DEFAULT_INDEX!, query: { match: { pathname: current.name } }, }); @@ -121,6 +153,9 @@ export class CabinetController extends Controller { return this.setStatus(HttpStatusCode.NO_CONTENT); } + /** + * @example cabinetName "ตู้เอกสาร 1" + */ @Delete("/{cabinetName}") @Tags("ตู้เอกสาร") @Security("bearerAuth", ["admin"]) diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index 9c56b10..4e2ff8f 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -205,6 +205,24 @@ "$ref": "#/components/schemas/StorageFolder" }, "type": "array" + }, + "examples": { + "Example 1": { + "value": [ + { + "path": "ตู้เอกสาร 1/", + "name": "ตู้เอกสาร 1", + "createdAt": "2021-07-20T12:33:13.018Z", + "createdBy": "admin" + }, + { + "path": "ตู้เอกสาร 2/", + "name": "ตู้เอกสาร 2", + "createdAt": "2022-01-23T16:05:02.114Z", + "createdBy": "admin" + } + ] + } } } } @@ -251,7 +269,8 @@ "schema": { "properties": { "name": { - "type": "string" + "type": "string", + "example": "ตู้เอกสาร 1" } }, "required": [ @@ -292,7 +311,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" } ], "requestBody": { @@ -302,7 +322,8 @@ "schema": { "properties": { "name": { - "type": "string" + "type": "string", + "example": "ตู้เอกสารใหม่" } }, "required": [ @@ -341,7 +362,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" } ] } @@ -382,7 +404,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" } ] }, @@ -416,7 +439,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" } ], "requestBody": { @@ -467,7 +491,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" }, { "in": "path", @@ -521,7 +546,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" }, { "in": "path", From 0c933745bd41b2662222c66ca444e6352b509c5d Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:39:34 +0700 Subject: [PATCH 20/29] chore: format --- Services/server/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Services/server/package.json b/Services/server/package.json index 20efe44..b567676 100644 --- a/Services/server/package.json +++ b/Services/server/package.json @@ -9,8 +9,8 @@ "build": "tsoa spec-and-route && tsc", "preview": "node ./dist/app.js", "serve": "node ./dist/app.js", - "docs":"npm run docs:typedoc && npm run docs:swagger", - "docs:typedoc":"typedoc && scp -r be-typedoc projects-doc:~/projects/project-docs/edm/ && rm -r be-typedoc" , + "docs": "npm run docs:typedoc && npm run docs:swagger", + "docs:typedoc": "typedoc && scp -r be-typedoc projects-doc:~/projects/project-docs/edm/ && rm -r be-typedoc", "docs:swagger": "scp src/swagger.json projects-doc:~/projects/project-docs/edm/" }, "keywords": [], From 69c664087e0ea50a510a556489ed57bf9a9bd75e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:39:44 +0700 Subject: [PATCH 21/29] chore: loggin word --- Services/server/src/app.ts | 2 +- Services/server/src/rabbitmq/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Services/server/src/app.ts b/Services/server/src/app.ts index 24231e7..10d2042 100644 --- a/Services/server/src/app.ts +++ b/Services/server/src/app.ts @@ -28,7 +28,7 @@ app.use(swaggerSpecs.basePath, router); app.use(errorHandler); app.listen(PORT, "0.0.0.0", () => - console.log(`Application is running on http://localhost:${PORT}`), + console.log(`[APP] Application is running on http://localhost:${PORT}`), ); rabbitmq.init(amqHandler).catch((e) => console.error(e)); diff --git a/Services/server/src/rabbitmq/index.ts b/Services/server/src/rabbitmq/index.ts index 8f5a1f1..e900210 100644 --- a/Services/server/src/rabbitmq/index.ts +++ b/Services/server/src/rabbitmq/index.ts @@ -12,7 +12,7 @@ export async function init(cb: (key: string, event: string) => boolean | Promise channel.assertQueue(queue, { durable: true }); channel.prefetch(1); - console.log("[RabbitMQ] Listening for message..."); + console.log("[AMQ] Listening for message..."); channel.consume( queue, From 51817c91f57b1625df34a25da9d671dcc74d74ef Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:05:34 +0700 Subject: [PATCH 22/29] chore: .env example --- Services/server/.env.example | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Services/server/.env.example b/Services/server/.env.example index f2eb5d1..0eef5b5 100644 --- a/Services/server/.env.example +++ b/Services/server/.env.example @@ -1,5 +1,4 @@ PUBLIC_KEY= -REALM_URL= PORT= @@ -15,3 +14,7 @@ ELASTICSEARCH_HOST=localhost ELASTICSEARCH_PORT=9200 ELASTICSEARCH_INDEX= +AMQ_URL=amqp://admin:1234@localhost:9999 +AMQ_QUEUE=queue + +AUTH_BYPASS=false # MUST NOT TURN THIS ON IN PRODUCTION From ca5be0042dddb13c193fad172abc4b678d7fd2a7 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:06:01 +0700 Subject: [PATCH 23/29] feat: tools to do some automate prepare --- Services/server/tools/prepare.ts | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 Services/server/tools/prepare.ts diff --git a/Services/server/tools/prepare.ts b/Services/server/tools/prepare.ts new file mode 100644 index 0000000..d1394ef --- /dev/null +++ b/Services/server/tools/prepare.ts @@ -0,0 +1,44 @@ +import "dotenv/config"; +import esClient from "../src/elasticsearch"; +import minioClient from "../src/minio"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); + +esClient.ingest.putPipeline({ + id: "attachment", + body: { + description: "Extract attachment information", + processors: [ + { + attachment: { + field: "data", + }, + }, + { + remove: { + field: "data", + }, + }, + ], + }, +}); + +esClient.indices.putMapping({ + index: DEFAULT_INDEX, + body: { + properties: { + pathname: { + type: "keyword", + }, + }, + }, +}); + +minioClient.makeBucket(DEFAULT_BUCKET!, (e) => { + if (!e) console.log("Configuration needed for Bucket Notification to AMQP"); + console.error(e); +}); From 1559226837de83ee198538d80715851d75a327c1 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:07:32 +0700 Subject: [PATCH 24/29] refactor: is path exist function --- .../src/controllers/drawerController.ts | 60 +++++++++++++++---- .../server/src/controllers/fileController.ts | 5 +- .../src/controllers/folderController.ts | 17 ++---- .../src/controllers/subFolderController.ts | 17 ++---- .../controllers/subFolderFileController.ts | 5 +- Services/server/src/routes.ts | 2 + Services/server/src/swagger.json | 30 ++++++++-- Services/server/src/utils/minio.ts | 4 +- 8 files changed, 96 insertions(+), 44 deletions(-) diff --git a/Services/server/src/controllers/drawerController.ts b/Services/server/src/controllers/drawerController.ts index 8e8df67..746c045 100644 --- a/Services/server/src/controllers/drawerController.ts +++ b/Services/server/src/controllers/drawerController.ts @@ -12,12 +12,13 @@ import { Tags, Request, Response, + Example, } from "tsoa"; import minioClient from "../minio"; import esClient from "../elasticsearch"; -import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; +import { copyCond, listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio"; import HttpStatusCode from "../interfaces/http-status"; import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; @@ -31,6 +32,9 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." @Route("/cabinet/{cabinetName}/drawer") export class DrawerController extends Controller { + /** + * @example cabinetName "ตู้เอกสาร 1" + */ @Get("/") @Tags("ลิ้นชัก") @Security("bearerAuth") @@ -39,6 +43,20 @@ export class DrawerController extends Controller { "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง", ) @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") + @Example([ + { + path: "ตู้เอกสาร 1/ลิ้นชัก 1/", + name: "ลิ้นชัก 1", + createdAt: "2021-07-20T12:33:13.018Z", + createdBy: "admin", + }, + { + path: "ตู้เอกสาร 1/ลิ้นชัก 2/", + name: "ลิ้นชัก 2", + createdAt: "2022-01-23T16:05:02.114Z", + createdBy: "admin", + }, + ]) public async listDrawer(@Path() cabinetName: string): Promise { const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/`).catch((e) => console.error(`Error List Folder: ${e}`), @@ -48,6 +66,9 @@ export class DrawerController extends Controller { return list; } + /** + * @example cabinetName "ตู้เอกสาร 1" + */ @Post("/") @Tags("ลิ้นชัก") @Security("bearerAuth", ["admin"]) @@ -57,18 +78,17 @@ export class DrawerController extends Controller { public async createDrawer( @Request() request: { user: { preferred_username: string } }, @Path() cabinetName: string, - @Body() body: { name: string }, + @Body() + body: { + /** + * @example "ลิ้นชัก 1" + */ + name: string; + }, ) { const basePath = `${cabinetName}/`; - if ( - !Boolean( - await minioClient.statObject(DEFAULT_BUCKET!, `${basePath}.keep`).catch((e) => { - if (e.code === "NotFound") return false; - throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); - }), - ) - ) { + if (!(await pathExist(DEFAULT_BUCKET!, basePath))) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างลิ้นชัก"); } @@ -84,6 +104,10 @@ export class DrawerController extends Controller { return this.setStatus(HttpStatusCode.CREATED); } + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + */ @Put("/{drawerName}") @Tags("ลิ้นชัก") @Security("bearerAuth", ["admin"]) @@ -92,7 +116,13 @@ export class DrawerController extends Controller { public async editDrawer( @Path() cabinetName: string, @Path() drawerName: string, - @Body() body: { name: string }, + @Body() + body: { + /** + * @example "ลิ้นชักใหม่" + */ + name: string; + }, ): Promise { const path = `${cabinetName}/${drawerName}/`; const list = await listItem(DEFAULT_BUCKET!, path, true); @@ -113,7 +143,9 @@ export class DrawerController extends Controller { return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); } - const search = await esClient.search }>({ + const search = await esClient.search< + StorageFile & { attachment: Record } + >({ index: DEFAULT_INDEX!, query: { match: { pathname: current.name } }, }); @@ -140,6 +172,10 @@ export class DrawerController extends Controller { return this.setStatus(HttpStatusCode.NO_CONTENT); } + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + */ @Delete("/{drawerName}") @Tags("ลิ้นชัก") @Security("bearerAuth", ["admin"]) diff --git a/Services/server/src/controllers/fileController.ts b/Services/server/src/controllers/fileController.ts index b764926..945917c 100644 --- a/Services/server/src/controllers/fileController.ts +++ b/Services/server/src/controllers/fileController.ts @@ -84,9 +84,10 @@ export class FileController extends Controller { @Path() drawerName: string, @Path() folderName: string, ) { - const pathname = `${cabinetName}/${drawerName}/${folderName}/${body.file}`; + const basePath = `${cabinetName}/${drawerName}/${folderName}/`; + const pathname = `${basePath}${body.file}`; - if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/`))) { + if (!(await pathExist(DEFAULT_BUCKET!, basePath))) { throw new HttpError( HttpStatusCode.NOT_FOUND, "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ", diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index ebeef78..d67f64f 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -17,7 +17,7 @@ import { import minioClient from "../minio"; import esClient from "../elasticsearch"; -import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; +import { copyCond, listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio"; import HttpStatusCode from "../interfaces/http-status"; import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; @@ -64,15 +64,8 @@ export class FolderController extends Controller { ) { const basePath = `${cabinetName}/${drawerName}/`; - if ( - !Boolean( - await minioClient.statObject(DEFAULT_BUCKET!, `${basePath}.keep`).catch((e) => { - if (e.code === "NotFound") return false; - throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); - }), - ) - ) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม"); + if (!(await pathExist(DEFAULT_BUCKET!, basePath))) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างลิ้นชัก"); } const created = await minioClient @@ -117,7 +110,9 @@ export class FolderController extends Controller { return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); } - const search = await esClient.search }>({ + const search = await esClient.search< + StorageFile & { attachment: Record } + >({ index: DEFAULT_INDEX!, query: { match: { pathname: current.name } }, }); diff --git a/Services/server/src/controllers/subFolderController.ts b/Services/server/src/controllers/subFolderController.ts index 43dc1fa..59c7900 100644 --- a/Services/server/src/controllers/subFolderController.ts +++ b/Services/server/src/controllers/subFolderController.ts @@ -17,7 +17,7 @@ import { import minioClient from "../minio"; import esClient from "../elasticsearch"; -import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; +import { copyCond, listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio"; import HttpStatusCode from "../interfaces/http-status"; import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; @@ -67,15 +67,8 @@ export class SubFolderController extends Controller { ) { const basePath = `${cabinetName}/${drawerName}/${folderName}/`; - if ( - !Boolean( - await minioClient.statObject(DEFAULT_BUCKET!, `${basePath}.keep`).catch((e) => { - if (e.code === "NotFound") return false; - throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); - }), - ) - ) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบแฟ้ม"); + if (!(await pathExist(DEFAULT_BUCKET!, basePath))) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างลิ้นชัก"); } const created = await minioClient @@ -121,7 +114,9 @@ export class SubFolderController extends Controller { return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); } - const search = await esClient.search }>({ + const search = await esClient.search< + StorageFile & { attachment: Record } + >({ index: DEFAULT_INDEX!, query: { match: { pathname: current.name } }, }); diff --git a/Services/server/src/controllers/subFolderFileController.ts b/Services/server/src/controllers/subFolderFileController.ts index bebfb15..19c5def 100644 --- a/Services/server/src/controllers/subFolderFileController.ts +++ b/Services/server/src/controllers/subFolderFileController.ts @@ -88,9 +88,10 @@ export class SubFolderFileController extends Controller { @Path() folderName: string, @Path() subFolderName: string, ) { - const pathname = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${body.file}`; + const basePath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`; + const pathname = `${basePath}${body.file}`; - if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/${subFolderName}`))) { + if (!(await pathExist(DEFAULT_BUCKET!, basePath))) { throw new HttpError( HttpStatusCode.NOT_FOUND, "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ", diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index 396e16c..2dfd5a0 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -13,6 +13,8 @@ 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 { StorageController } from './controllers/storageController'; +// 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'; diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index 4e2ff8f..f2f9970 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -381,6 +381,24 @@ "$ref": "#/components/schemas/StorageFolder" }, "type": "array" + }, + "examples": { + "Example 1": { + "value": [ + { + "path": "ตู้เอกสาร 1/ลิ้นชัก 1/", + "name": "ลิ้นชัก 1", + "createdAt": "2021-07-20T12:33:13.018Z", + "createdBy": "admin" + }, + { + "path": "ตู้เอกสาร 1/ลิ้นชัก 2/", + "name": "ลิ้นชัก 2", + "createdAt": "2022-01-23T16:05:02.114Z", + "createdBy": "admin" + } + ] + } } } } @@ -450,7 +468,8 @@ "schema": { "properties": { "name": { - "type": "string" + "type": "string", + "example": "ลิ้นชัก 1" } }, "required": [ @@ -500,7 +519,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ลิ้นชัก 1" } ], "requestBody": { @@ -510,7 +530,8 @@ "schema": { "properties": { "name": { - "type": "string" + "type": "string", + "example": "ลิ้นชักใหม่" } }, "required": [ @@ -555,7 +576,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ลิ้นชัก 1" } ] } diff --git a/Services/server/src/utils/minio.ts b/Services/server/src/utils/minio.ts index d3d9088..d01369d 100644 --- a/Services/server/src/utils/minio.ts +++ b/Services/server/src/utils/minio.ts @@ -15,9 +15,9 @@ export function replaceIllegalChars(path: string, replace = "-") { /** * Check if folder really exist by using ".keep" object. */ -export async function pathExist(path: string): Promise { +export async function pathExist(bucket: string, path: string): Promise { return Boolean( - await minioClient.statObject("ehr", `${path.replace(/^\/|\/$/g, "")}/.keep`).catch((e) => { + await minioClient.statObject(bucket, `${path.replace(/^\/|\/$/g, "")}/.keep`).catch((e) => { if (e.code === "NotFound") return false; throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); }), From 314155f11551317b7cf9fd43e3f99c5117e9fa0e Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:40:10 +0700 Subject: [PATCH 25/29] docs: drawer --- .../src/controllers/folderController.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index d67f64f..7fb5f79 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -31,6 +31,10 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder") export class FolderController extends Controller { + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + */ @Get("/") @Tags("แฟ้ม") @Security("bearerAuth") @@ -50,6 +54,10 @@ export class FolderController extends Controller { return list; } + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + */ @Post("/") @Tags("แฟ้ม") @Security("bearerAuth", ["admin"]) @@ -58,7 +66,13 @@ export class FolderController extends Controller { @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createFolder( @Request() request: { user: { preferred_username: string } }, - @Body() body: { name: string }, + @Body() + body: { + /** + * @example "แฟ้ม 1" + */ + name: string; + }, @Path() cabinetName: string, @Path() drawerName: string, ) { @@ -80,13 +94,24 @@ export class FolderController extends Controller { return this.setStatus(HttpStatusCode.CREATED); } + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + * @example folderName "แฟ้ม 1" + */ @Put("/{folderName}") @Tags("แฟ้ม") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editFolder( - @Body() body: { name: string }, + @Body() + body: { + /** + * @example "แฟ้มใหม่" + */ + name: string; + }, @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @@ -139,6 +164,11 @@ export class FolderController extends Controller { return this.setStatus(HttpStatusCode.NO_CONTENT); } + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + * @example folderName "แฟ้ม 1" + */ @Delete("/{folderName}") @Tags("แฟ้ม") @Security("bearerAuth", ["admin"]) From 55ee26d3d77004c122d9ce95ba90f8f75150e230 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:41:27 +0700 Subject: [PATCH 26/29] chore: updated swagger gen --- Services/server/src/swagger.json | 36 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index f2f9970..4bcff55 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -1116,7 +1116,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" }, { "in": "path", @@ -1124,7 +1125,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ลิ้นชัก 1" } ] }, @@ -1158,7 +1160,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" }, { "in": "path", @@ -1166,7 +1169,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ลิ้นชัก 1" } ], "requestBody": { @@ -1176,7 +1180,8 @@ "schema": { "properties": { "name": { - "type": "string" + "type": "string", + "example": "แฟ้ม 1" } }, "required": [ @@ -1217,7 +1222,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" }, { "in": "path", @@ -1225,7 +1231,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ลิ้นชัก 1" }, { "in": "path", @@ -1233,7 +1240,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "แฟ้ม 1" } ], "requestBody": { @@ -1243,7 +1251,8 @@ "schema": { "properties": { "name": { - "type": "string" + "type": "string", + "example": "แฟ้มใหม่" } }, "required": [ @@ -1279,7 +1288,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" }, { "in": "path", @@ -1287,7 +1297,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ลิ้นชัก 1" }, { "in": "path", @@ -1295,7 +1306,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "แฟ้ม 1" } ] } From 17b70e8d5e4c38cba583efabb3f98f63c8449b8a Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:55:52 +0700 Subject: [PATCH 27/29] docs: swagger folder and subfolder --- .../src/controllers/folderController.ts | 15 ++++ .../src/controllers/subFolderController.ts | 45 ++++++++++- Services/server/src/swagger.json | 81 +++++++++++++++---- 3 files changed, 125 insertions(+), 16 deletions(-) diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index 7fb5f79..b0e5abf 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -12,6 +12,7 @@ import { Tags, Request, Response, + Example, } from "tsoa"; import minioClient from "../minio"; @@ -43,6 +44,20 @@ export class FolderController extends Controller { "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง", ) @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") + @Example([ + { + path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1", + name: "แฟ้ม 1", + createdAt: "2021-07-20T12:33:13.018Z", + createdBy: "admin", + }, + { + path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 2", + name: "แฟ้ม 2", + createdAt: "2022-01-23T16:05:02.114Z", + createdBy: "admin", + }, + ]) public async listFolder( @Path() cabinetName: string, @Path() drawerName: string, diff --git a/Services/server/src/controllers/subFolderController.ts b/Services/server/src/controllers/subFolderController.ts index 59c7900..b748d16 100644 --- a/Services/server/src/controllers/subFolderController.ts +++ b/Services/server/src/controllers/subFolderController.ts @@ -12,6 +12,7 @@ import { Tags, Request, Response, + Example, } from "tsoa"; import minioClient from "../minio"; @@ -31,6 +32,11 @@ if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified." @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") export class SubFolderController extends Controller { + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + * @example folderName "แฟ้ม 1" + */ @Get("/") @Tags("แฟ้มย่อย") @Security("bearerAuth") @@ -39,6 +45,20 @@ export class SubFolderController extends Controller { "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง", ) @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") + @Example([ + { + path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/แฟ้มย่อย 1", + name: "แฟ้มย่อย 1", + createdAt: "2021-07-20T12:33:13.018Z", + createdBy: "admin", + }, + { + path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/แฟ้มย่อย 2", + name: "แฟ้มย่อย 2", + createdAt: "2022-01-23T16:05:02.114Z", + createdBy: "admin", + }, + ]) public async listFolder( @Path() cabinetName: string, @Path() drawerName: string, @@ -52,6 +72,11 @@ export class SubFolderController extends Controller { return list; } + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + * @example folderName "แฟ้ม 1" + */ @Post("/") @Tags("แฟ้มย่อย") @Security("bearerAuth", ["admin"]) @@ -83,13 +108,25 @@ export class SubFolderController extends Controller { return this.setStatus(HttpStatusCode.CREATED); } + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + * @example folderName "แฟ้ม 1" + * @example subFolderName "แฟ้มย่อย 1" + */ @Put("/{subFolderName}") @Tags("แฟ้มย่อย") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editFolder( - @Body() body: { name: string }, + @Body() + body: { + /** + * @example "แฟ้มใหม่" + */ + name: string; + }, @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @@ -143,6 +180,12 @@ export class SubFolderController extends Controller { return this.setStatus(HttpStatusCode.NO_CONTENT); } + /** + * @example cabinetName "ตู้เอกสาร 1" + * @example drawerName "ลิ้นชัก 1" + * @example folderName "แฟ้ม 1" + * @example subFolderName "แฟ้มย่อย 1" + */ @Delete("/{subFolderName}") @Tags("แฟ้มย่อย") @Security("bearerAuth", ["admin"]) diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index 4bcff55..08bfd48 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -1093,6 +1093,24 @@ "$ref": "#/components/schemas/StorageFolder" }, "type": "array" + }, + "examples": { + "Example 1": { + "value": [ + { + "path": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1", + "name": "แฟ้ม 1", + "createdAt": "2021-07-20T12:33:13.018Z", + "createdBy": "admin" + }, + { + "path": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 2", + "name": "แฟ้ม 2", + "createdAt": "2022-01-23T16:05:02.114Z", + "createdBy": "admin" + } + ] + } } } } @@ -1364,6 +1382,24 @@ "$ref": "#/components/schemas/StorageFolder" }, "type": "array" + }, + "examples": { + "Example 1": { + "value": [ + { + "path": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/แฟ้มย่อย 1", + "name": "แฟ้มย่อย 1", + "createdAt": "2021-07-20T12:33:13.018Z", + "createdBy": "admin" + }, + { + "path": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/แฟ้มย่อย 2", + "name": "แฟ้มย่อย 2", + "createdAt": "2022-01-23T16:05:02.114Z", + "createdBy": "admin" + } + ] + } } } } @@ -1387,7 +1423,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" }, { "in": "path", @@ -1395,7 +1432,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ลิ้นชัก 1" }, { "in": "path", @@ -1403,7 +1441,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "แฟ้ม 1" } ] }, @@ -1437,7 +1476,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" }, { "in": "path", @@ -1445,7 +1485,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ลิ้นชัก 1" }, { "in": "path", @@ -1453,7 +1494,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "แฟ้ม 1" } ], "requestBody": { @@ -1504,7 +1546,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" }, { "in": "path", @@ -1512,7 +1555,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ลิ้นชัก 1" }, { "in": "path", @@ -1520,7 +1564,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "แฟ้ม 1" }, { "in": "path", @@ -1528,7 +1573,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "แฟ้มย่อย 1" } ], "requestBody": { @@ -1538,7 +1584,8 @@ "schema": { "properties": { "name": { - "type": "string" + "type": "string", + "example": "แฟ้มใหม่" } }, "required": [ @@ -1574,7 +1621,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ตู้เอกสาร 1" }, { "in": "path", @@ -1582,7 +1630,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "ลิ้นชัก 1" }, { "in": "path", @@ -1590,7 +1639,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "แฟ้ม 1" }, { "in": "path", @@ -1598,7 +1648,8 @@ "required": true, "schema": { "type": "string" - } + }, + "example": "แฟ้มย่อย 1" } ] } From bc13f0e5ce4b9b7dfb17a642f430777ef7734f57 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 17:30:20 +0700 Subject: [PATCH 28/29] refactor: optional metadata field (for file upload only) --- .../server/src/controllers/fileController.ts | 17 +++++++++-------- .../controllers/subFolderFileController.ts | 16 ++++++++-------- Services/server/src/routes.ts | 4 ++-- Services/server/src/swagger.json | 19 +++---------------- 4 files changed, 22 insertions(+), 34 deletions(-) diff --git a/Services/server/src/controllers/fileController.ts b/Services/server/src/controllers/fileController.ts index 945917c..4ecf5cb 100644 --- a/Services/server/src/controllers/fileController.ts +++ b/Services/server/src/controllers/fileController.ts @@ -75,10 +75,10 @@ export class FileController extends Controller { @Body() body: { file: string; - title: string; - description: string; - category: string; - keyword: string; + title?: string; + description?: string; + category?: string; + keyword?: string; }, @Path() cabinetName: string, @Path() drawerName: string, @@ -118,10 +118,10 @@ export class FileController extends Controller { fileName: body.file, fileSize: 0, fileType: "", - title: body.title, - description: body.description, - category: body.category.split(","), - keyword: body.keyword.split(","), + title: body.title ?? "", + description: body.description ?? "", + category: body.category?.split(",") ?? [], + keyword: body.keyword?.split(",") ?? [], upload: false, createdAt: new Date().toISOString(), createdBy: rec ? rec.createdBy : "n/a", @@ -148,6 +148,7 @@ export class FileController extends Controller { @Tags("ไฟล์") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม") + @Response(HttpStatusCode.NO_CONTENT, "สำเร็จ") @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async updateFile( @Request() request: { user: { preferred_username: string } }, diff --git a/Services/server/src/controllers/subFolderFileController.ts b/Services/server/src/controllers/subFolderFileController.ts index 19c5def..7d9ca33 100644 --- a/Services/server/src/controllers/subFolderFileController.ts +++ b/Services/server/src/controllers/subFolderFileController.ts @@ -78,10 +78,10 @@ export class SubFolderFileController extends Controller { @Body() body: { file: string; - title: string; - description: string; - category: string; - keyword: string; + title?: string; + description?: string; + category?: string; + keyword?: string; }, @Path() cabinetName: string, @Path() drawerName: string, @@ -122,10 +122,10 @@ export class SubFolderFileController extends Controller { fileName: body.file, fileSize: 0, fileType: "", - title: body.title, - description: body.description, - category: body.category.split(","), - keyword: body.keyword.split(","), + title: body.title ?? "", + description: body.description ?? "", + category: body.category?.split(",") ?? [], + keyword: body.keyword?.split(",") ?? [], upload: false, createdAt: new Date().toISOString(), createdBy: rec ? rec.createdBy : "n/a", diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index 2dfd5a0..845fb8b 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -326,7 +326,7 @@ 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"}, - body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string","required":true},"category":{"dataType":"string","required":true},"description":{"dataType":"string","required":true},"title":{"dataType":"string","required":true},"file":{"dataType":"string","required":true}}}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string"},"category":{"dataType":"string"},"description":{"dataType":"string"},"title":{"dataType":"string"},"file":{"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"}, @@ -730,7 +730,7 @@ export function RegisterRoutes(app: Router) { function SubFolderFileController_uploadFile(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":{"keyword":{"dataType":"string","required":true},"category":{"dataType":"string","required":true},"description":{"dataType":"string","required":true},"title":{"dataType":"string","required":true},"file":{"dataType":"string","required":true}}}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string"},"category":{"dataType":"string"},"description":{"dataType":"string"},"title":{"dataType":"string"},"file":{"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"}, diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index 08bfd48..ebdf260 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -692,10 +692,6 @@ } }, "required": [ - "keyword", - "category", - "description", - "title", "file", "upload", "updatedBy", @@ -771,10 +767,6 @@ } }, "required": [ - "keyword", - "category", - "description", - "title", "file" ], "type": "object" @@ -811,6 +803,9 @@ } } }, + "204": { + "description": "สำเร็จ" + }, "404": { "description": "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม" } @@ -1772,10 +1767,6 @@ } }, "required": [ - "keyword", - "category", - "description", - "title", "file", "upload", "updatedBy", @@ -1859,10 +1850,6 @@ } }, "required": [ - "keyword", - "category", - "description", - "title", "file" ], "type": "object" From 79f004681aa8dd1d319bd33b50dfd15cda6e0ee9 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 28 Nov 2023 08:58:06 +0700 Subject: [PATCH 29/29] wip: dynamic system --- .../src/controllers/storageController.ts | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 Services/server/src/controllers/storageController.ts diff --git a/Services/server/src/controllers/storageController.ts b/Services/server/src/controllers/storageController.ts new file mode 100644 index 0000000..4a1c32d --- /dev/null +++ b/Services/server/src/controllers/storageController.ts @@ -0,0 +1,104 @@ +import { Body, Controller, Get, Path, Post, Put, Query, Route, SuccessResponse } from "tsoa"; +import { replaceIllegalChars } from "../utils/minio"; +import minioClient from "../minio"; +import HttpStatusCode from "../interfaces/http-status"; +import HttpError from "../interfaces/http-error"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); + +@Route("/storage/d") +export class StorageController extends Controller { + @Get() + @SuccessResponse(HttpStatusCode.OK, "Success") + public async getFolder(@Query() path: string, @Query() bucket?: string) { + const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => { + const item: { pathname: string; name: string }[] = []; + + const stream = minioClient.listObjectsV2(bucket ?? DEFAULT_BUCKET!, path); + stream.on("data", (v) => { + if (v && v.prefix) { + item.push({ + pathname: v.prefix, + name: v.prefix.slice(path?.length).split("/")[0], + }); + } + }); + stream.on("end", () => resolve(item)); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"))); + }); + + return list; + } + + @Post() + @SuccessResponse(HttpStatusCode.CREATED, "Success") + public async createFolder(@Query() path: string, @Query() bucket?: string) { + const fragments = path.split("/").filter(Boolean); + + await Promise.all( + fragments.map(async (_, i, a) => { + const path = [...a.slice(0, i + 1)].map((x) => replaceIllegalChars(x)).join("/"); + const created = await minioClient + .putObject(bucket ?? DEFAULT_BUCKET!, `${path}/.keep`, "", 0) + .catch((e) => console.error(e)); + if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ); + + return this.setStatus(HttpStatusCode.CREATED); + } + + @Put() + @SuccessResponse(HttpStatusCode.NO_CONTENT, "Success") + public async updateFolder( + @Body() + body: { + from: { + bucket: string; + path: string; + }; + to: { + bucket: string; + path: string; + }; + }, + ) { + const src = body.from.path.split("/").filter(Boolean).join("/"); + const dst = body.to.path.split("/").filter(Boolean).join("/"); + + if ( + !Boolean( + await minioClient + .statObject(DEFAULT_BUCKET!, `${dst.replace(/^\/|\/$/g, "")}/.keep`) + .catch((e) => { + if (e.code === "NotFound") return false; + throw new Error(`Minio Error: ${e}`); + }), + ) + ) + throw new HttpError(HttpStatusCode.NOT_FOUND, "Destination Not Found"); + + const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => { + const item: { pathname: string; name: string }[] = []; + + const stream = minioClient.listObjectsV2(body.from.bucket, src); + + stream.on("data", (v) => { + if (v && v.prefix) { + item.push({ + pathname: v.prefix, + name: v.prefix.slice(src.length).split("/")[0], + }); + } + }); + stream.on("error", (e) => reject(new Error(`Minio Error: ${e}`))); + stream.on("end", () => resolve(item)); + }); + + return this.setStatus(HttpStatusCode.NO_CONTENT); + } +}