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 { RegisterRoutes } from "./routes";
import errorHandler from "./middlewares/exception"; import errorHandler from "./middlewares/exception";
import rabbitmq from "./rabbitmq";
import swaggerSpecs from "./swagger.json"; import swaggerSpecs from "./swagger.json";
import { handler as amqHandler } from "./rabbitmq/handler";
const PORT = +(process.env.PORT || 80); const PORT = +(process.env.PORT || 80);
@ -28,3 +30,5 @@ app.use(errorHandler);
app.listen(PORT, "0.0.0.0", () => app.listen(PORT, "0.0.0.0", () =>
console.log(`Application is running on http://localhost:${PORT}`), 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, SuccessResponse,
Tags, Tags,
Request, Request,
Response,
} from "tsoa"; } from "tsoa";
import * as Minio from "minio";
import minioClient from "../storage";
import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; import minioClient from "../minio";
import HttpStatusCode from "../interfaces/http-status";
import { listFolder, listItem, replaceIllegalChars } from "../utils/minio";
import esClient from "../elasticsearch"; 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") @Route("cabinet")
export class CabinetController extends Controller { export class CabinetController extends Controller {
@Get("/") @Get("/")
@Tags("Cabinet") @Tags("Cabinet")
@SuccessResponse(HttpStatusCode.OK) @Security("bearerAuth")
@Response(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง",
)
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async listCabinet(): Promise<EhrFolder[]> { public async listCabinet(): Promise<EhrFolder[]> {
const list = await listFolder().catch((e) => console.error(`Error List Folder: ${e}`)); const list = await listFolder(DEFAULT_BUCKET!).catch((e) =>
console.error(`Error List Folder: ${e}`),
if (!list) { );
throw new Error("Error listing folder"); if (!list)
} throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง");
return list; return list;
} }
@Post("/") @Post("/")
@Tags("Cabinet") @Tags("Cabinet")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.CREATED) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
public async createCabinet( public async createCabinet(
@Request() request: { user: { preferred_username: string } }, @Request() request: { user: { preferred_username: string } },
@Body() body: { name: string }, @Body() body: { name: string },
) { ) {
const uploaded = await minioClient const created = await minioClient
.putObject("ehr", `${replaceIllegalChars(body.name)}/.keep`, "", 0, { .putObject(DEFAULT_BUCKET!, `${replaceIllegalChars(body.name)}/.keep`, "", 0, {
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username, createdBy: request.user.preferred_username,
}) })
.catch((e) => console.error(e)); .catch((e) => console.error(e));
if (!uploaded) throw new Error("Object storage error occured."); if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
return this.setStatus(HttpStatusCode.CREATED); return this.setStatus(HttpStatusCode.CREATED);
} }
@Put("/{cabinetName}") @Put("/{cabinetName}")
@Tags("Cabinet") @Tags("Cabinet")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "Success") @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async editCabinet( public async editCabinet(
@Path() cabinetName: string, @Path() cabinetName: string,
@Body() body: { name: string }, @Body() body: { name: string },
): Promise<void> { ): Promise<void> {
const list = await listItem(`${cabinetName}/`, true); const path = `${cabinetName}/`;
const list = await listItem(DEFAULT_BUCKET!, path, true);
const cond = new Minio.CopyConditions();
await Promise.all( await Promise.all(
list.map(async (current) => { list.map(async (current) => {
if (!current.name) return; if (!current.name) return;
const destination = `${replaceIllegalChars(body.name)}/${current.name.slice( const destination = `${replaceIllegalChars(body.name)}/${current.name.slice(path.length)}`;
cabinetName.length + 1, const source = `/${DEFAULT_BUCKET}/${current.name}`;
)}`;
const source = `/ehr/${current.name}`;
return await minioClient return await minioClient
.copyObject("ehr", destination, source, cond) .copyObject(DEFAULT_BUCKET!, destination, source, copyCond)
.then(async () => { .then(async () => {
if (!current.name) return; if (current.name.includes(".keep")) {
return await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
await minioClient.removeObject("ehr", current.name); }
if (current.name.includes(".keep")) return;
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({ const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", index: DEFAULT_INDEX!,
query: { query: { match: { pathname: current.name } },
match: {
pathname: current.name,
},
},
}); });
if (search && search.hits.hits.length === 0) { if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล");
throw new Error("Data cannot be found in database.");
}
const data = search.hits.hits[0]; const data = search.hits.hits[0];
await esClient.update({ await esClient.update({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", index: DEFAULT_INDEX!,
id: data._id, id: data._id,
doc: { pathname: destination }, doc: { pathname: destination },
}); });
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
throw new Error("Failed to move."); throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
}); });
}), }),
); );
@ -118,44 +123,23 @@ export class CabinetController extends Controller {
@Delete("/{cabinetName}") @Delete("/{cabinetName}")
@Tags("Cabinet") @Tags("Cabinet")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteCabinet(@Path() cabinetName: string) { public async deleteCabinet(@Path() cabinetName: string) {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const objects: string[] = []; const objects: string[] = [];
const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/`, true); const stream = minioClient.listObjectsV2(DEFAULT_BUCKET!, `${cabinetName}/`, true);
stream.on("data", (v) => { stream.on("data", (v) => {
if (!(v && v.name)) return; if (v && v.name) objects.push(v.name);
objects.push(v.name);
}); });
stream.on("close", async () =>
stream.on("close", async () => { resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)),
minioClient.removeObjects("ehr", objects); );
resolve(); stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")));
});
stream.on("error", () => reject(new Error("Object storage error occured.")));
}); });
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); return this.setStatus(HttpStatusCode.NO_CONTENT);
} }
} }

View file

@ -6,120 +6,133 @@ import {
Path, Path,
Post, Post,
Put, Put,
Request,
Route, Route,
Security, Security,
SuccessResponse, SuccessResponse,
Tags, Tags,
Request,
Response,
} from "tsoa"; } 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 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 { 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") @Route("/cabinet/{cabinetName}/drawer")
export class DrawerController extends Controller { export class DrawerController extends Controller {
@Get("/") @Get("/")
@Tags("Drawer") @Tags("Drawer")
@SuccessResponse(HttpStatusCode.OK) @Security("bearerAuth")
@Response(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง",
)
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async listDrawer(@Path() cabinetName: string): Promise<EhrFolder[]> { public async listDrawer(@Path() cabinetName: string): Promise<EhrFolder[]> {
const fullpath = [cabinetName, ""].join("/"); const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/`).catch((e) =>
console.error(`Error List Folder: ${e}`),
if (!(await pathExist(fullpath))) { );
throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); if (!list)
} throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง");
return list;
return listFolder(fullpath);
} }
@Post("/") @Post("/")
@Tags("Drawer") @Tags("Drawer")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.CREATED) @Response(HttpStatusCode.NOT_FOUND, "ไม่พบลิ้นชัก")
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
public async createDrawer( public async createDrawer(
@Request() request: { user: { preferred_username: string } }, @Request() request: { user: { preferred_username: string } },
@Path() cabinetName: string, @Path() cabinetName: string,
@Body() body: { name: string }, @Body() body: { name: string },
) { ) {
if (!(await pathExist(`${cabinetName}/`))) { const basePath = `${cabinetName}/`;
throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet cannot be found.");
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 const created = await minioClient
.putObject("ehr", `${cabinetName}/${replaceIllegalChars(body.name)}/.keep`, "", 0, { .putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, {
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username, createdBy: request.user.preferred_username,
}) })
.catch((e) => console.error(e)); .catch((e) => console.error(e));
if (!uploaded) { if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
throw new Error("Object storage error occured.");
}
return this.setStatus(HttpStatusCode.CREATED); return this.setStatus(HttpStatusCode.CREATED);
} }
@Put("/{drawerName}") @Put("/{drawerName}")
@Tags("Drawer") @Tags("Drawer")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async editDrawer( public async editDrawer(
@Path() cabinetName: string, @Path() cabinetName: string,
@Path() drawerName: string, @Path() drawerName: string,
@Body() body: { name: string }, @Body() body: { name: string },
): Promise<void> { ): Promise<void> {
const fullpath = `${cabinetName}/${drawerName}/`; const path = `${cabinetName}/${drawerName}/`;
const list = await listItem(DEFAULT_BUCKET!, path, true);
const list = await listItem(fullpath, true);
const cond = new Minio.CopyConditions();
await Promise.all( await Promise.all(
list.map(async (current) => { list.map(async (current) => {
if (!current.name) return; if (!current.name) return;
const destination = `${cabinetName}/${replaceIllegalChars(body.name)}/${current.name.slice( 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 return await minioClient
.copyObject("ehr", destination, source, cond) .copyObject(DEFAULT_BUCKET!, destination, source, copyCond)
.then(async () => { .then(async () => {
if (!current.name) return; if (current.name.includes(".keep")) {
return await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
await minioClient.removeObject("ehr", current.name); }
if (current.name.includes(".keep")) return;
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({ const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", index: DEFAULT_INDEX!,
query: { query: { match: { pathname: current.name } },
match: {
pathname: current.name,
},
},
}); });
if (search && search.hits.hits.length === 0) { if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล");
throw new Error("Data cannot be found in database.");
}
const data = search.hits.hits[0]; const data = search.hits.hits[0];
await esClient.update({ await esClient.update({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", index: DEFAULT_INDEX!,
id: data._id, id: data._id,
doc: { pathname: destination }, doc: { pathname: destination },
}); });
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
throw new Error("Failed to move."); throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
}); });
}), }),
); );
@ -129,44 +142,26 @@ export class DrawerController extends Controller {
@Delete("/{drawerName}") @Delete("/{drawerName}")
@Tags("Drawer") @Tags("Drawer")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT) @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) { public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const objects: string[] = []; const objects: string[] = [];
const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/${drawerName}/`, true); const stream = minioClient.listObjectsV2(
DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/`,
true,
);
stream.on("data", (v) => { stream.on("data", (v) => {
if (!(v && v.name)) return; if (v && v.name) objects.push(v.name);
objects.push(v.name);
}); });
stream.on("close", async () =>
stream.on("close", async () => { resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)),
minioClient.removeObjects("ehr", objects); );
resolve(); stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")));
});
stream.on("error", () => reject(new Error("Object storage error occured.")));
}); });
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); return this.setStatus(HttpStatusCode.NO_CONTENT);
} }
} }

View file

@ -6,88 +6,100 @@ import {
Path, Path,
Post, Post,
Put, Put,
Request,
Route, Route,
Security, Security,
SuccessResponse, SuccessResponse,
Tags, Tags,
Request,
Response,
} from "tsoa"; } from "tsoa";
import * as Minio from "minio";
import HttpError from "../interfaces/http-error"; import minioClient from "../minio";
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 esClient from "../elasticsearch"; 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") @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder")
export class FolderController extends Controller { export class FolderController extends Controller {
@Get("/") @Get("/")
@Tags("Folder") @Tags("Folder")
@SuccessResponse(HttpStatusCode.OK) @Security("bearerAuth")
@Response(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง",
)
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async listFolder( public async listFolder(
@Path() cabinetName: string, @Path() cabinetName: string,
@Path() drawerName: string, @Path() drawerName: string,
): Promise<EhrFolder[]> { ): Promise<EhrFolder[]> {
const fullpath = [cabinetName, drawerName, ""].join("/"); const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/${drawerName}`).catch((e) =>
console.error(`Error List Folder: ${e}`),
if (!(await pathExist(fullpath))) { );
throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง");
} return list;
return listFolder(fullpath);
} }
@Post("/") @Post("/")
@Tags("Folder") @Tags("Folder")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.CREATED) @Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม")
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
public async createFolder( public async createFolder(
@Request() request: { user: { preferred_username: string } }, @Request() request: { user: { preferred_username: string } },
@Body() body: { name: string }, @Body() body: { name: string },
@Path() cabinetName: string, @Path() cabinetName: string,
@Path() drawerName: string, @Path() drawerName: string,
) { ) {
if (!(await pathExist(`${cabinetName}/${drawerName}/`))) { const basePath = `${cabinetName}/${drawerName}/`;
throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet or drawer cannot be found.");
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 const created = await minioClient
.putObject( .putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, {
"ehr", createdAt: new Date().toISOString(),
`${cabinetName}/${drawerName}/${replaceIllegalChars(body.name)}/.keep`, createdBy: request.user.preferred_username,
"", })
0,
{
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
},
)
.catch((e) => console.error(e)); .catch((e) => console.error(e));
if (!uploaded) { if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
throw new Error("Object storage error occured.");
}
return this.setStatus(HttpStatusCode.CREATED); return this.setStatus(HttpStatusCode.CREATED);
} }
@Put("/{folderName}") @Put("/{folderName}")
@Tags("Folder") @Tags("Folder")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async editFolder( public async editFolder(
@Body() body: { name: string }, @Body() body: { name: string },
@Path() cabinetName: string, @Path() cabinetName: string,
@Path() drawerName: string, @Path() drawerName: string,
@Path() folderName: string, @Path() folderName: string,
) { ) {
const fullpath = `${cabinetName}/${drawerName}/${folderName}/`; const path = `${cabinetName}/${drawerName}/${folderName}`;
const list = await listItem(DEFAULT_BUCKET!, path, true);
const list = await listItem(fullpath, true);
const cond = new Minio.CopyConditions();
await Promise.all( await Promise.all(
list.map(async (current) => { list.map(async (current) => {
@ -95,42 +107,36 @@ export class FolderController extends Controller {
const destination = `${cabinetName}/${drawerName}/${replaceIllegalChars( const destination = `${cabinetName}/${drawerName}/${replaceIllegalChars(
body.name, body.name,
)}/${current.name.slice(fullpath.length)}`; )}/${current.name.slice(path.length)}`;
const source = `/ehr/${current.name}`; const source = `/${DEFAULT_BUCKET}/${current.name}`;
return await minioClient return await minioClient
.copyObject("ehr", destination, source, cond) .copyObject(DEFAULT_BUCKET!, destination, source, copyCond)
.then(async () => { .then(async () => {
if (!current.name) return; if (current.name.includes(".keep")) {
return await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
await minioClient.removeObject("ehr", current.name); }
if (current.name.includes(".keep")) return;
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({ const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", index: DEFAULT_INDEX!,
query: { query: { match: { pathname: current.name } },
match: {
pathname: current.name,
},
},
}); });
if (search && search.hits.hits.length === 0) { if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล");
throw new Error("Data cannot be found in database.");
}
const data = search.hits.hits[0]; const data = search.hits.hits[0];
await esClient.update({ await esClient.update({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", index: DEFAULT_INDEX!,
id: data._id, id: data._id,
doc: { pathname: destination }, doc: { pathname: destination },
}); });
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
throw new Error("Failed to move."); throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
}); });
}), }),
); );
@ -140,8 +146,8 @@ export class FolderController extends Controller {
@Delete("/{folderName}") @Delete("/{folderName}")
@Tags("Folder") @Tags("Folder")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT) @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteFolder( public async deleteFolder(
@Path() cabinetName: string, @Path() cabinetName: string,
@Path() drawerName: string, @Path() drawerName: string,
@ -150,42 +156,20 @@ export class FolderController extends Controller {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const objects: string[] = []; const objects: string[] = [];
const stream = minioClient.listObjectsV2( const stream = minioClient.listObjectsV2(
"ehr", DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/${folderName}/`, `${cabinetName}/${drawerName}/${folderName}`,
true, true,
); );
stream.on("data", (v) => { stream.on("data", (v) => {
if (!(v && v.name)) return; if (v && v.name) objects.push(v.name);
objects.push(v.name);
}); });
stream.on("close", async () =>
stream.on("close", async () => { resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)),
minioClient.removeObjects("ehr", objects); );
resolve(); stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")));
});
stream.on("error", () => reject(new Error("Object storage error occured.")));
}); });
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); return this.setStatus(HttpStatusCode.NO_CONTENT);
} }
} }

View file

@ -6,44 +6,58 @@ import {
Path, Path,
Post, Post,
Put, Put,
Request,
Route, Route,
Security, Security,
SuccessResponse, SuccessResponse,
Tags, Tags,
Request,
Response,
} from "tsoa"; } from "tsoa";
import * as Minio from "minio";
import HttpError from "../interfaces/http-error"; import minioClient from "../minio";
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 esClient from "../elasticsearch"; 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") @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder")
export class SubFolderController extends Controller { export class SubFolderController extends Controller {
@Get("/") @Get("/")
@Tags("SubFolder") @Tags("SubFolder")
@SuccessResponse(HttpStatusCode.OK) @Security("bearerAuth")
@Response(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง",
)
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async listFolder( public async listFolder(
@Path() cabinetName: string, @Path() cabinetName: string,
@Path() drawerName: string, @Path() drawerName: string,
@Path() folderName: string, @Path() folderName: string,
): Promise<EhrFolder[]> { ): Promise<EhrFolder[]> {
const fullpath = [cabinetName, drawerName, folderName, ""].join("/"); const list = await listFolder(
DEFAULT_BUCKET!,
if (!(await pathExist(fullpath))) { `${cabinetName}/${drawerName}/${folderName}`,
throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); ).catch((e) => console.error(`Error List Folder: ${e}`));
} if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง");
return list;
return listFolder(fullpath);
} }
@Post("/") @Post("/")
@Tags("SubFolder") @Tags("SubFolder")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.CREATED) @Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม")
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
public async createFolder( public async createFolder(
@Request() request: { user: { preferred_username: string } }, @Request() request: { user: { preferred_username: string } },
@Body() body: { name: string }, @Body() body: { name: string },
@ -51,37 +65,36 @@ export class SubFolderController extends Controller {
@Path() drawerName: string, @Path() drawerName: string,
@Path() folderName: string, @Path() folderName: string,
) { ) {
if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}`))) { const basePath = `${cabinetName}/${drawerName}/${folderName}/`;
throw new HttpError(
HttpStatusCode.PRECONDITION_FAILED, if (
"Cabinet, drawer or folder cannot be found.", !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 const created = await minioClient
.putObject( .putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, {
"ehr", createdAt: new Date().toISOString(),
`${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(body.name)}/.keep`, createdBy: request.user.preferred_username,
"", })
0,
{
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
},
)
.catch((e) => console.error(e)); .catch((e) => console.error(e));
if (!uploaded) { if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
throw new Error("Object storage error occured.");
}
return this.setStatus(HttpStatusCode.CREATED); return this.setStatus(HttpStatusCode.CREATED);
} }
@Put("/{subFolderName}") @Put("/{subFolderName}")
@Tags("SubFolder") @Tags("SubFolder")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT) @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async editFolder( public async editFolder(
@Body() body: { name: string }, @Body() body: { name: string },
@Path() cabinetName: string, @Path() cabinetName: string,
@ -89,11 +102,8 @@ export class SubFolderController extends Controller {
@Path() folderName: string, @Path() folderName: string,
@Path() subFolderName: string, @Path() subFolderName: string,
) { ) {
const fullpath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`; const path = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`;
const list = await listItem(DEFAULT_BUCKET!, path, true);
const list = await listItem(fullpath, true);
const cond = new Minio.CopyConditions();
await Promise.all( await Promise.all(
list.map(async (current) => { list.map(async (current) => {
@ -101,42 +111,36 @@ export class SubFolderController extends Controller {
const destination = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars( const destination = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(
body.name, body.name,
)}/${current.name.slice(fullpath.length)}`; )}/${current.name.slice(path.length)}`;
const source = `/ehr/${current.name}`; const source = `/${DEFAULT_BUCKET}/${current.name}`;
return await minioClient return await minioClient
.copyObject("ehr", destination, source, cond) .copyObject(DEFAULT_BUCKET!, destination, source, copyCond)
.then(async () => { .then(async () => {
if (!current.name) return; if (current.name.includes(".keep")) {
return await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
await minioClient.removeObject("ehr", current.name); }
if (current.name.includes(".keep")) return;
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({ const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", index: DEFAULT_INDEX!,
query: { query: { match: { pathname: current.name } },
match: {
pathname: current.name,
},
},
}); });
if (search && search.hits.hits.length === 0) { if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล");
throw new Error("Data cannot be found in database.");
}
const data = search.hits.hits[0]; const data = search.hits.hits[0];
await esClient.update({ await esClient.update({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", index: DEFAULT_INDEX!,
id: data._id, id: data._id,
doc: { pathname: destination }, doc: { pathname: destination },
}); });
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
throw new Error("Failed to move."); throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
}); });
}), }),
); );
@ -146,8 +150,8 @@ export class SubFolderController extends Controller {
@Delete("/{subFolderName}") @Delete("/{subFolderName}")
@Tags("SubFolder") @Tags("SubFolder")
@Security("bearerAuth") @Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT) @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteFolder( public async deleteFolder(
@Path() cabinetName: string, @Path() cabinetName: string,
@Path() drawerName: string, @Path() drawerName: string,
@ -157,42 +161,20 @@ export class SubFolderController extends Controller {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const objects: string[] = []; const objects: string[] = [];
const stream = minioClient.listObjectsV2( const stream = minioClient.listObjectsV2(
"ehr", DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`, `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`,
true, true,
); );
stream.on("data", (v) => { stream.on("data", (v) => {
if (!(v && v.name)) return; if (v && v.name) objects.push(v.name);
objects.push(v.name);
}); });
stream.on("close", async () =>
stream.on("close", async () => { resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)),
minioClient.removeObjects("ehr", objects); );
resolve(); stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")));
});
stream.on("error", () => reject(new Error("Object storage error occured.")));
}); });
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); return this.setStatus(HttpStatusCode.NO_CONTENT);
} }
} }

View file

@ -1,17 +1,25 @@
import { EhrFile } from "../interfaces/ehr-fs"; import { EhrFile } from "../interfaces/ehr-fs";
import esClient from "../elasticsearch"; 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 // for failed queue that will come later
const cachedBuffer: Record<string, Buffer> = {}; const cachedBuffer: Record<string, Buffer> = {};
const cachedMetadata: Record<string, { size: number; type: string }> = {}; const cachedMetadata: Record<string, { size: number; type: string }> = {};
export async function handler(key: string): Promise<boolean> { export async function handler(key: string, event: string): Promise<boolean> {
console.info(`[AMQ] Messages received - key: ${key}`); console.info(`[AMQ] Messages received - key: ${key}, event: ${event}`);
const [bucket, ...fragment] = key.split("/"); const [bucket, ...fragment] = key.split("/");
const pathname = fragment.join("/"); const pathname = fragment.join("/");
if (event === "s3:ObjectRemoved:Delete") {
return await ensureDelete(pathname);
}
if (!cachedBuffer[key]) { if (!cachedBuffer[key]) {
const stream = await minioClient.getObject(bucket, pathname); const stream = await minioClient.getObject(bucket, pathname);
const buffer = Buffer.concat(await stream.toArray()); const buffer = Buffer.concat(await stream.toArray());
@ -41,7 +49,7 @@ export async function handler(key: string): Promise<boolean> {
async function popInfo(pathname: string) { async function popInfo(pathname: string) {
const result = await esClient const result = await esClient
.search<EhrFile & { attachment?: Record<string, unknown> }>({ .search<EhrFile & { attachment?: Record<string, unknown> }>({
index: "my-test-index", index: DEFAULT_INDEX!,
query: { match: { pathname } }, query: { match: { pathname } },
}) })
.catch((e) => console.error(e)); .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) { if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) {
await esClient await esClient
.delete({ .delete({
index: "my-test-index", index: DEFAULT_INDEX!,
id: result.hits.hits[0]._id, id: result.hits.hits[0]._id,
}) })
.catch((e) => console.error(e)); .catch((e) => console.error(e));
@ -63,6 +71,20 @@ async function popInfo(pathname: string) {
return false; 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. * Handle when record in elasticsearch cannot be found.
* This will insert empty metadata. * This will insert empty metadata.
@ -94,7 +116,7 @@ async function handleNotFoundRecord(
const result = await esClient const result = await esClient
.index({ .index({
pipeline: "attachment", pipeline: "attachment",
index: "my-test-index", index: DEFAULT_INDEX!,
document: { data: base64, ...metadata }, document: { data: base64, ...metadata },
}) })
.catch((e) => console.error(e)); .catch((e) => console.error(e));
@ -116,7 +138,7 @@ async function handleFoundRecord(
const result = await esClient const result = await esClient
.index({ .index({
pipeline: "attachment", pipeline: "attachment",
index: "my-test-index", index: DEFAULT_INDEX!,
document: { data: Buffer.from(buffer).toString("base64"), ...metadata }, document: { data: Buffer.from(buffer).toString("base64"), ...metadata },
}) })
.catch((e) => console.error(e)); .catch((e) => console.error(e));

View file

@ -1,6 +1,6 @@
import amqp from "amqplib"; 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; if (!process.env.AMQ_URL || !process.env.AMQ_QUEUE) return;
const { AMQ_URL: url, AMQ_QUEUE: queue } = process.env; 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()); 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.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 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)); 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 { export default { init };
init,
};

View file

@ -48,6 +48,7 @@ const models: TsoaRoute.Models = {
"description": {"dataType":"string","required":true}, "description": {"dataType":"string","required":true},
"category": {"dataType":"array","array":{"dataType":"string"},"required":true}, "category": {"dataType":"array","array":{"dataType":"string"},"required":true},
"keyword": {"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}, "updatedAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true},
"updatedBy": {"dataType":"string","required":true}, "updatedBy": {"dataType":"string","required":true},
"createdAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"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 // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa
// ########################################################################################################### // ###########################################################################################################
app.get('/cabinet', app.get('/cabinet',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(CabinetController)), ...(fetchMiddlewares<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.listCabinet)), ...(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 // 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', app.post('/cabinet',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(CabinetController)), ...(fetchMiddlewares<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.createCabinet)), ...(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 // 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', app.put('/cabinet/:cabinetName',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(CabinetController)), ...(fetchMiddlewares<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.editCabinet)), ...(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 // 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', app.delete('/cabinet/:cabinetName',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(CabinetController)), ...(fetchMiddlewares<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.deleteCabinet)), ...(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 // 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', app.get('/cabinet/:cabinetName/drawer',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(DrawerController)), ...(fetchMiddlewares<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.listDrawer)), ...(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 // 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', app.post('/cabinet/:cabinetName/drawer',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(DrawerController)), ...(fetchMiddlewares<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.createDrawer)), ...(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 // 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', app.put('/cabinet/:cabinetName/drawer/:drawerName',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(DrawerController)), ...(fetchMiddlewares<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.editDrawer)), ...(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 // 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', app.delete('/cabinet/:cabinetName/drawer/:drawerName',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(DrawerController)), ...(fetchMiddlewares<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.deleteDrawer)), ...(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 // 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', app.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":[]}]),
upload.single('file'),
...(fetchMiddlewares<RequestHandler>(FileController)), ...(fetchMiddlewares<RequestHandler>(FileController)),
...(fetchMiddlewares<RequestHandler>(FileController.prototype.uploadFile)), ...(fetchMiddlewares<RequestHandler>(FileController.prototype.uploadFile)),
function FileController_uploadFile(request: any, response: any, next: any) { function FileController_uploadFile(request: any, response: any, next: any) {
const args = { const args = {
request: {"in":"request","name":"request","required":true,"dataType":"object"}, request: {"in":"request","name":"request","required":true,"dataType":"object"},
file: {"in":"formData","name":"file","required":true,"dataType":"file"}, 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}}},
title: {"in":"formData","name":"title","required":true,"dataType":"string"},
description: {"in":"formData","name":"description","required":true,"dataType":"string"},
keyword: {"in":"formData","name":"keyword","required":true,"dataType":"string"},
category: {"in":"formData","name":"category","required":true,"dataType":"string"},
cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"},
drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"},
folderName: {"in":"path","name":"folderName","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 // 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', app.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":[]}]),
upload.single('file'),
...(fetchMiddlewares<RequestHandler>(FileController)), ...(fetchMiddlewares<RequestHandler>(FileController)),
...(fetchMiddlewares<RequestHandler>(FileController.prototype.updateFile)), ...(fetchMiddlewares<RequestHandler>(FileController.prototype.updateFile)),
@ -362,11 +359,7 @@ export function RegisterRoutes(app: Router) {
drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"},
folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"},
fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"}, fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"},
file: {"in":"formData","name":"file","dataType":"file"}, 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"}}},
title: {"in":"formData","name":"title","dataType":"string"},
description: {"in":"formData","name":"description","dataType":"string"},
keyword: {"in":"formData","name":"keyword","dataType":"string"},
category: {"in":"formData","name":"category","dataType":"string"},
}; };
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa // 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 // 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', app.get('/cabinet/:cabinetName/drawer/:drawerName/folder',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(FolderController)), ...(fetchMiddlewares<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.listFolder)), ...(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 // 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', app.post('/cabinet/:cabinetName/drawer/:drawerName/folder',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(FolderController)), ...(fetchMiddlewares<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.createFolder)), ...(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 // 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', app.put('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(FolderController)), ...(fetchMiddlewares<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.editFolder)), ...(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 // 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', app.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(FolderController)), ...(fetchMiddlewares<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.deleteFolder)), ...(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 // 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', app.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderController)), ...(fetchMiddlewares<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.listFolder)), ...(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 // 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', app.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderController)), ...(fetchMiddlewares<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.createFolder)), ...(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 // 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', app.put('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderController)), ...(fetchMiddlewares<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.editFolder)), ...(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 // 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', app.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName',
authenticateMiddleware([{"bearerAuth":[]}]), authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderController)), ...(fetchMiddlewares<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.deleteFolder)), ...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.deleteFolder)),

View file

@ -79,6 +79,9 @@
}, },
"type": "array" "type": "array"
}, },
"upload": {
"type": "boolean"
},
"updatedAt": { "updatedAt": {
"anyOf": [ "anyOf": [
{ {
@ -117,6 +120,7 @@
"description", "description",
"category", "category",
"keyword", "keyword",
"upload",
"updatedAt", "updatedAt",
"updatedBy", "updatedBy",
"createdAt", "createdAt",
@ -178,9 +182,9 @@
} }
}, },
"info": { "info": {
"title": "BMA EHR - Test Service API", "title": "Enterprise Document Management(EDM) - API",
"version": "0.0.1", "version": "0.0.2",
"description": "Best practice for initialize express project", "description": "Open API Specfication for Enterprise Document Management ",
"license": { "license": {
"name": "by Frappet", "name": "by Frappet",
"url": "https://frappet.com" "url": "https://frappet.com"
@ -193,7 +197,7 @@
"operationId": "ListCabinet", "operationId": "ListCabinet",
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "สำเร็จ",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@ -204,19 +208,9 @@
} }
} }
} }
} },
}, "500": {
"tags": [ "description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง"
"Cabinet"
],
"security": [],
"parameters": []
},
"post": {
"operationId": "CreateCabinet",
"responses": {
"201": {
"description": ""
} }
}, },
"tags": [ "tags": [
@ -227,6 +221,28 @@
"bearerAuth": [] "bearerAuth": []
} }
], ],
"parameters": []
},
"post": {
"operationId": "CreateCabinet",
"responses": {
"201": {
"description": "สำเร็จ"
},
"500": {
"description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์"
}
},
"tags": [
"Cabinet"
],
"security": [
{
"bearerAuth": [
"admin"
]
}
],
"parameters": [], "parameters": [],
"requestBody": { "requestBody": {
"required": true, "required": true,
@ -253,7 +269,10 @@
"operationId": "EditCabinet", "operationId": "EditCabinet",
"responses": { "responses": {
"204": { "204": {
"description": "Success" "description": "สำเร็จ"
},
"500": {
"description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้"
} }
}, },
"tags": [ "tags": [
@ -261,7 +280,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -297,12 +318,10 @@
"operationId": "DeleteCabinet", "operationId": "DeleteCabinet",
"responses": { "responses": {
"204": { "204": {
"description": "", "description": "สำเร็จ"
"content": { },
"application/json": { "500": {
"schema": {} "description": "เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"
}
}
} }
}, },
"tags": [ "tags": [
@ -310,7 +329,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -330,7 +351,7 @@
"operationId": "ListDrawer", "operationId": "ListDrawer",
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "สำเร็จ",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@ -341,12 +362,19 @@
} }
} }
} }
},
"500": {
"description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง"
} }
}, },
"tags": [ "tags": [
"Drawer" "Drawer"
], ],
"security": [], "security": [
{
"bearerAuth": []
}
],
"parameters": [ "parameters": [
{ {
"in": "path", "in": "path",
@ -362,7 +390,13 @@
"operationId": "CreateDrawer", "operationId": "CreateDrawer",
"responses": { "responses": {
"201": { "201": {
"description": "" "description": "สำเร็จ"
},
"404": {
"description": "ไม่พบลิ้นชัก"
},
"500": {
"description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์"
} }
}, },
"tags": [ "tags": [
@ -370,7 +404,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -408,7 +444,10 @@
"operationId": "EditDrawer", "operationId": "EditDrawer",
"responses": { "responses": {
"204": { "204": {
"description": "" "description": "สำเร็จ"
},
"500": {
"description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้"
} }
}, },
"tags": [ "tags": [
@ -416,7 +455,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -460,12 +501,7 @@
"operationId": "DeleteDrawer", "operationId": "DeleteDrawer",
"responses": { "responses": {
"204": { "204": {
"description": "", "description": "สำเร็จ"
"content": {
"application/json": {
"schema": {}
}
}
} }
}, },
"tags": [ "tags": [
@ -473,7 +509,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -501,7 +539,74 @@
"operationId": "UploadFile", "operationId": "UploadFile",
"responses": { "responses": {
"201": { "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": [ "tags": [
@ -541,34 +646,33 @@
"requestBody": { "requestBody": {
"required": true, "required": true,
"content": { "content": {
"multipart/form-data": { "application/json": {
"schema": { "schema": {
"type": "object",
"properties": { "properties": {
"file": {
"type": "string",
"format": "binary"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"keyword": { "keyword": {
"type": "string" "type": "string"
}, },
"category": { "category": {
"type": "string" "type": "string"
},
"description": {
"type": "string"
},
"title": {
"type": "string"
},
"file": {
"type": "string"
} }
}, },
"required": [ "required": [
"file",
"title",
"description",
"keyword", "keyword",
"category" "category",
] "description",
"title",
"file"
],
"type": "object"
} }
} }
} }
@ -628,7 +732,27 @@
"operationId": "UpdateFile", "operationId": "UpdateFile",
"responses": { "responses": {
"200": { "200": {
"description": "" "description": "",
"content": {
"application/json": {
"schema": {
"anyOf": [
{},
{
"properties": {
"upload": {
"type": "string"
}
},
"required": [
"upload"
],
"type": "object"
}
]
}
}
}
} }
}, },
"tags": [ "tags": [
@ -674,29 +798,28 @@
} }
], ],
"requestBody": { "requestBody": {
"required": false, "required": true,
"content": { "content": {
"multipart/form-data": { "application/json": {
"schema": { "schema": {
"type": "object",
"properties": { "properties": {
"file": {
"type": "string",
"format": "binary"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"keyword": { "keyword": {
"type": "string" "type": "string"
}, },
"category": { "category": {
"type": "string" "type": "string"
},
"description": {
"type": "string"
},
"title": {
"type": "string"
},
"file": {
"type": "string"
} }
} },
"type": "object"
} }
} }
} }
@ -789,6 +912,9 @@
} }
] ]
}, },
"upload": {
"type": "boolean"
},
"keyword": { "keyword": {
"items": { "items": {
"type": "string" "type": "string"
@ -829,6 +955,7 @@
"createdAt", "createdAt",
"updatedBy", "updatedBy",
"updatedAt", "updatedAt",
"upload",
"keyword", "keyword",
"category", "category",
"description", "description",
@ -890,7 +1017,7 @@
"operationId": "ListFolder", "operationId": "ListFolder",
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "สำเร็จ",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@ -901,12 +1028,19 @@
} }
} }
} }
},
"500": {
"description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง"
} }
}, },
"tags": [ "tags": [
"Folder" "Folder"
], ],
"security": [], "security": [
{
"bearerAuth": []
}
],
"parameters": [ "parameters": [
{ {
"in": "path", "in": "path",
@ -930,7 +1064,13 @@
"operationId": "CreateFolder", "operationId": "CreateFolder",
"responses": { "responses": {
"201": { "201": {
"description": "" "description": "สำเร็จ"
},
"404": {
"description": "ไม่พบของแฟ้ม"
},
"500": {
"description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์"
} }
}, },
"tags": [ "tags": [
@ -938,7 +1078,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -984,7 +1126,10 @@
"operationId": "EditFolder", "operationId": "EditFolder",
"responses": { "responses": {
"204": { "204": {
"description": "" "description": "สำเร็จ"
},
"500": {
"description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้"
} }
}, },
"tags": [ "tags": [
@ -992,7 +1137,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -1044,12 +1191,7 @@
"operationId": "DeleteFolder", "operationId": "DeleteFolder",
"responses": { "responses": {
"204": { "204": {
"description": "", "description": "สำเร็จ"
"content": {
"application/json": {
"schema": {}
}
}
} }
}, },
"tags": [ "tags": [
@ -1057,7 +1199,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -1128,7 +1272,7 @@
"operationId": "ListFolder", "operationId": "ListFolder",
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "สำเร็จ",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@ -1139,12 +1283,19 @@
} }
} }
} }
},
"500": {
"description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง"
} }
}, },
"tags": [ "tags": [
"SubFolder" "SubFolder"
], ],
"security": [], "security": [
{
"bearerAuth": []
}
],
"parameters": [ "parameters": [
{ {
"in": "path", "in": "path",
@ -1176,7 +1327,13 @@
"operationId": "CreateFolder", "operationId": "CreateFolder",
"responses": { "responses": {
"201": { "201": {
"description": "" "description": "สำเร็จ"
},
"404": {
"description": "ไม่พบของแฟ้ม"
},
"500": {
"description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์"
} }
}, },
"tags": [ "tags": [
@ -1184,7 +1341,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -1238,7 +1397,10 @@
"operationId": "EditFolder", "operationId": "EditFolder",
"responses": { "responses": {
"204": { "204": {
"description": "" "description": "สำเร็จ"
},
"500": {
"description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้"
} }
}, },
"tags": [ "tags": [
@ -1246,7 +1408,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -1306,12 +1470,7 @@
"operationId": "DeleteFolder", "operationId": "DeleteFolder",
"responses": { "responses": {
"204": { "204": {
"description": "", "description": "สำเร็จ"
"content": {
"application/json": {
"schema": {}
}
}
} }
}, },
"tags": [ "tags": [
@ -1319,7 +1478,9 @@
], ],
"security": [ "security": [
{ {
"bearerAuth": [] "bearerAuth": [
"admin"
]
} }
], ],
"parameters": [ "parameters": [
@ -1482,6 +1643,9 @@
} }
] ]
}, },
"upload": {
"type": "boolean"
},
"keyword": { "keyword": {
"items": { "items": {
"type": "string" "type": "string"
@ -1519,6 +1683,7 @@
"createdAt", "createdAt",
"updatedBy", "updatedBy",
"updatedAt", "updatedAt",
"upload",
"keyword", "keyword",
"category", "category",
"description", "description",
@ -1758,6 +1923,9 @@
} }
] ]
}, },
"upload": {
"type": "boolean"
},
"keyword": { "keyword": {
"items": { "items": {
"type": "string" "type": "string"
@ -1798,6 +1966,7 @@
"createdAt", "createdAt",
"updatedBy", "updatedBy",
"updatedAt", "updatedAt",
"upload",
"keyword", "keyword",
"category", "category",
"description", "description",

View file

@ -14,32 +14,30 @@ const jwtVerify = createVerifier({
}, },
}); });
export function expressAuthentication( export async function expressAuthentication(
request: express.Request, request: express.Request,
securityName: string, securityName: string,
scopes?: string[], scopes?: string[],
) { ) {
return new Promise(async (resolve, reject) => { if (process.env.AUTH_BYPASS) return { preferred_username: "bypassed" };
if (securityName !== "bearerAuth") reject(new Error("Unknown authentication method."));
const token = request.headers["authorization"]?.includes("Bearer ") if (securityName !== "bearerAuth") throw new Error("Unknown authentication method.");
? request.headers["authorization"].split(" ")[1]
: null;
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) { const payload = await jwtVerify(token).catch((_) => null);
return reject(new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided."));
}
if (scopes && !scopes.every((v) => payload.resource_access[payload.azp].roles.includes(v))) { if (!payload) {
return reject( throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.");
new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action."), }
);
}
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 { EhrFolder } from "../interfaces/ehr-fs";
import * as Minio from "minio"; 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 * Replace illegal character eg. ? % < > / \ : | that can't be in path with other char with dash by default.
* @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
* @param path - string to check and replace * @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 = "-") { export function replaceIllegalChars(path: string, replace = "-") {
return path.replace(/[/\\?%*:|"<>]/g, replaceChar); return path.replace(/[/\\?%*:|"<>]/g, replace);
} }
/** /**
* Utility function to check for .keep file if it is exist or not. * Check if folder really exist by using ".keep" object.
* @returns true if .keep exist, false otherwise
*/ */
export async function pathExist(path: string): Promise<boolean> { export async function pathExist(path: string): Promise<boolean> {
return await minioClient return Boolean(
.statObject("ehr", `${safePath(path)}.keep`) await minioClient.statObject("ehr", `${path.replace(/^\/|\/$/g, "")}/.keep`).catch((e) => {
.then((_) => true)
.catch((e) => {
if (e.code === "NotFound") return false; 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 * @param path - path to list
* @return list of folder with metadata * @return list of folder with metadata
*/ */
export function listFolder(path?: string): Promise<EhrFolder[]> { export async function listFolder(bucket: string, path?: string): Promise<EhrFolder[]> {
if (path) path = safePath(path); if (path) path = `${path.replace(/^\/|\/$/g, "")}/`;
return new Promise((resolve, reject) => { const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => {
const folder: EhrFolder[] = []; const item: { pathname: string; name: string }[] = [];
const stream = minioClient.listObjectsV2("ehr", path ?? "");
const stream = minioClient.listObjectsV2(bucket, path ?? "");
stream.on("data", (v) => { stream.on("data", (v) => {
if (!(v && v.prefix)) return; if (v && v.prefix) {
item.push({
folder.push({ pathname: v.prefix,
pathname: v.prefix, name: v.prefix.slice(path?.length).split("/")[0],
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",
};
} }
resolve(folder);
}); });
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error("Object storage error occured."))); 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) => { return new Promise((resolve, reject) => {
const stream = minioClient.listObjectsV2("ehr", path, recursive); const stream = minioClient.listObjectsV2(bucket, path, recursive);
const item: Minio.BucketItem[] = []; const item: Minio.BucketItem[] = [];
stream.on("data", (v) => { stream.on("data", (v) => {
if (v && v.name) item.push(v); if (v && v.name) item.push(v);
}); });
stream.on("end", () => resolve(item)); 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();