refactor: rabbitmq implement

This commit is contained in:
Methapon2001 2023-11-27 09:45:30 +07:00
parent 24350a11a4
commit 3fc70daed0
No known key found for this signature in database
GPG key ID: 849924FEF46BD132
12 changed files with 676 additions and 545 deletions

View file

@ -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));

View file

@ -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<EhrFolder[]> {
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<void> {
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<EhrFile & { attachment: Record<string, string> }>({
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<void>((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);
}
}

View file

@ -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<EhrFolder[]> {
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<void> {
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<EhrFile & { attachment: Record<string, string> }>({
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<void>((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);
}
}

View file

@ -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<EhrFolder[]> {
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<EhrFile & { attachment: Record<string, string> }>({
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<void>((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);
}
}

View file

@ -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<EhrFolder[]> {
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<EhrFile & { attachment: Record<string, string> }>({
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<void>((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);
}
}

View file

@ -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<string, Buffer> = {};
const cachedMetadata: Record<string, { size: number; type: string }> = {};
export async function handler(key: string): Promise<boolean> {
console.info(`[AMQ] Messages received - key: ${key}`);
export async function handler(key: string, event: string): Promise<boolean> {
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<boolean> {
async function popInfo(pathname: string) {
const result = await esClient
.search<EhrFile & { attachment?: Record<string, unknown> }>({
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));

View file

@ -1,6 +1,6 @@
import amqp from "amqplib";
export async function init(cb: (key: string) => boolean | Promise<boolean>) {
export async function init(cb: (key: string, event: string) => boolean | Promise<boolean>) {
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<boolean>) {
const parsed: Record<string, unknown> = 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<boolean>) {
);
}
export default {
init,
};
export default { init };

View file

@ -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<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(FileController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(FileController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.deleteFolder)),

View file

@ -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",

View file

@ -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;
}

View file

@ -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<boolean> {
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<boolean> {
* @param path - path to list
* @return list of folder with metadata
*/
export function listFolder(path?: string): Promise<EhrFolder[]> {
if (path) path = safePath(path);
export async function listFolder(bucket: string, path?: string): Promise<EhrFolder[]> {
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<Minio.BucketItem[]> {
export async function listItem(
bucket: string,
path: string,
recursive = false,
): Promise<Minio.BucketItem[]> {
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();