From 56318f581c66f0ad33ed0758c72e8608f8c5c871 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Tue, 11 Mar 2025 09:13:42 +0700 Subject: [PATCH] feat: edm integrated document template --- src/controllers/00-doc-template-controller.ts | 34 +++- src/interfaces/edm.ts | 39 ++++ src/services/edm/edm-api.ts | 172 ++++++++++++++++++ 3 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/interfaces/edm.ts create mode 100644 src/services/edm/edm-api.ts diff --git a/src/controllers/00-doc-template-controller.ts b/src/controllers/00-doc-template-controller.ts index c0e6fff..303ead5 100644 --- a/src/controllers/00-doc-template-controller.ts +++ b/src/controllers/00-doc-template-controller.ts @@ -9,6 +9,9 @@ import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; import { getFileBuffer, listFile } from "../utils/minio"; import { dateFormat } from "../utils/datetime"; +import { downloadFile as edmDownloadFile, list as edmList } from "../services/edm/edm-api"; + +const DOCUMENT_PATH = process.env.DOCUMENT_TEMPLATE_LOCATION?.split("/").filter(Boolean) || []; const quotationData = (id: string) => prisma.quotation.findFirst({ @@ -66,6 +69,13 @@ const quotationData = (id: string) => export class DocTemplateController extends Controller { @Get() async getTemplate() { + if ( + process.env.DOCUMENT_TEMPLATE_PROVIDER && + process.env.DOCUMENT_TEMPLATE_PROVIDER === "edm-api" + ) { + const ret = await edmList("file", DOCUMENT_PATH); + if (ret) return ret.map((v) => v.fileName); + } return await listFile(`doc-template/`); } @@ -122,7 +132,29 @@ export class DocTemplateController extends Controller { if (!data) throw notFoundError("Data"); if (dataOnly) return record; - const template = await getFileBuffer(`doc-template/${documentTemplate}`); + let template: Buffer | null = null; + + switch (process.env.DOCUMENT_TEMPLATE_PROVIDER) { + case "edm-api": + await edmDownloadFile(DOCUMENT_PATH, documentTemplate).then(async (payload) => { + if (!payload) return; + const res = await fetch(payload.downloadUrl); + if (!res.ok) return; + template = Buffer.from(await res.arrayBuffer()); + }); + break; + case "local": + default: + template = await getFileBuffer(`${DOCUMENT_PATH.join("/")}/${documentTemplate}`); + } + + if (!template) { + throw new HttpError( + HttpStatus.INTERNAL_SERVER_ERROR, + "Failed to get template file", + "templateGetFailed", + ); + } if (!data) Readable.from(template); diff --git a/src/interfaces/edm.ts b/src/interfaces/edm.ts new file mode 100644 index 0000000..63d0769 --- /dev/null +++ b/src/interfaces/edm.ts @@ -0,0 +1,39 @@ +export interface StorageFolder { + /** + * @prop Full path to this folder. It is used as key as there are no files or directories at the same location. + */ + pathname: string; + /** + * @prop Directory / Folder name. + */ + name: string; + + createdAt: string | Date; + createdBy: string | Date; +} + +export interface StorageFile { + /** + * @prop Full path to this folder. It is used as key as there are no files or directories at the same location. + */ + pathname: string; + + fileName: string; + fileSize: number; + fileType: string; + + title: string; + description: string; + author: string; + category: string[]; + keyword: string[]; + metadata: Record; + + path: string; + upload: boolean; + + updatedAt: string | Date; + updatedBy: string; + createdAt: string | Date; + createdBy: string; +} diff --git a/src/services/edm/edm-api.ts b/src/services/edm/edm-api.ts new file mode 100644 index 0000000..3421db5 --- /dev/null +++ b/src/services/edm/edm-api.ts @@ -0,0 +1,172 @@ +import { DecodedJwt, createDecoder } from "fast-jwt"; +import HttpError from "../../interfaces/http-error"; +import HttpStatus from "../../interfaces/http-status"; +import { StorageFile, StorageFolder } from "../../interfaces/edm"; + +const jwtDecode = createDecoder({ complete: true }); + +export type FileProps = Partial< + Pick +> & { + metadata?: { [key: string]: unknown }; +}; + +const STORAGE_KEYCLOAK = process.env.EDM_KEYCLOAK!; +const STORAGE_KEYCLOAK_CLIENT = process.env.EDM_KEYCLOAK_CLIENT!; +const STORAGE_REALM = process.env.EDM_REALM!; +const STORAGE_URL = process.env.EDM_URL!; +const STORAGE_USER = process.env.EDM_ADMIN_USER!; +const STORAGE_PASSWORD = process.env.EDM_ADMIN_PASSWORD!; + +let token: string | null = null; +let decoded: DecodedJwt | null = null; + +/** + * Check if token is expired or will expire in 30 seconds + * @returns true if expire or can't get exp, false otherwise + */ +export function expireCheck(token: string, beforeExpire: number = 30) { + decoded = jwtDecode(token); + + if (decoded && decoded.payload.exp) { + return Date.now() / 1000 >= decoded.payload.exp - beforeExpire; + } + return true; +} + +/** + * Get token from id service if needed + */ +export async function getToken() { + if (!token || expireCheck(token)) { + const body = new URLSearchParams(); + + body.append("scope", "openid"); + body.append("grant_type", "password"); + body.append("client_id", STORAGE_KEYCLOAK_CLIENT || "edm"); + body.append("username", STORAGE_USER); + body.append("password", STORAGE_PASSWORD); + + const res = await fetch( + `${STORAGE_KEYCLOAK}/realms/${STORAGE_REALM}/protocol/openid-connect/token`, + { + method: "POST", + body: body, + }, + ).catch((e) => console.error(e)); + + if (!res) return; + + const data = await res.json(); + + if (data && data.access_token) { + token = data.access_token; + } + } + return token; +} + +/** + * @param path - Path that new folder will live + * @param name - Name of the folder to create + * @param recursive - Will create parent automatically + */ +export async function createFolder(path: string[], name: string, recursive: boolean = false) { + if (recursive && path.length > 0) { + await createFolder(path.slice(0, -1), path[path.length - 1], true); + } + + const res = await fetch(`${STORAGE_URL}/storage/folder`, { + method: "POST", + headers: { + Authorization: `Bearer ${await getToken()}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ path, name }), + }).catch((e) => console.error(e)); + + if (!res || !res.ok) { + return Boolean(console.error(res ? await res.json() : res)); + } + return true; +} + +/** + * @param path - Path that new file will live + * @param file - Name of the file to create + */ +export async function createFile(path: string[], file: string, props?: FileProps) { + const res = await fetch(`${STORAGE_URL}/storage/file`, { + method: "POST", + headers: { + Authorization: `Bearer ${await getToken()}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...props, + path, + file, + hidden: path.some((v) => v.startsWith(".")), + }), + }).catch((e) => console.error(e)); + + if (!res || !res.ok) { + return Boolean(console.error(res ? await res.json() : res)); + } + return (await res.json()) as StorageFile & { uploadUrl: string }; +} + +export async function list( + operation: "file" | "folder", + path: string[], +): Promise { + const res = await fetch(`${STORAGE_URL}/storage/list`, { + method: "POST", + headers: { + Authorization: `Bearer ${await getToken()}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ operation, path, hidden: true }), + }).catch((e) => console.error(e)); + + console.log(`${STORAGE_URL}/storage/list`); + + if (!res || !res.ok) { + if (res && res.status === HttpStatus.NOT_FOUND) { + return []; + } + return Boolean(console.error(res ? await res.json() : res)) as false; + } + return await res.json(); +} + +export async function listFolder(path: string[]) { + return (await list("folder", path)) as StorageFolder[] | boolean; +} + +export async function listFile(path: string[]) { + return (await list("file", path)) as StorageFile[] | boolean; +} + +export async function downloadFile( + path: string[], + file: string, +): Promise { + const res = await fetch(`${STORAGE_URL}/storage/file/download`, { + method: "POST", + headers: { + Authorization: `Bearer ${await getToken()}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ path, file }), + }).catch((e) => console.error(e)); + + if (!res || !res.ok) { + if (res && res.status === HttpStatus.NOT_FOUND) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบไฟล์ในระบบ"); + } + console.error(res ? await res.json() : res); + return false; + } + return await res.json(); +}