From 0ae376ca4b6bfa59e22481efc2b1f4c96174eb69 Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:11:01 +0700 Subject: [PATCH] Add EDM service --- src/interfaces/storage-fs.ts | 39 +++++++ src/services/edm.ts | 219 +++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 src/interfaces/storage-fs.ts create mode 100644 src/services/edm.ts diff --git a/src/interfaces/storage-fs.ts b/src/interfaces/storage-fs.ts new file mode 100644 index 0000000..63d0769 --- /dev/null +++ b/src/interfaces/storage-fs.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.ts b/src/services/edm.ts new file mode 100644 index 0000000..176c4da --- /dev/null +++ b/src/services/edm.ts @@ -0,0 +1,219 @@ +import { DecodedJwt, createDecoder } from "fast-jwt"; +import HttpError from "../interfaces/http-error"; +import HttpStatus from "../interfaces/http-status"; +import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; + +const jwtDecode = createDecoder({ complete: true }); + +if (!process.env.STORAGE_URL) { + throw new Error("Requires STORAGE_URL env variable."); +} + +if (!process.env.STORAGE_REALM_URL && !process.env.STORAGE_SECRET) { + throw new Error("Requires STORAGE_REALM_URL and STORAGE_SECRET env variable."); +} + +export type FileProps = Partial< + Pick +> & { + metadata?: { [key: string]: unknown }; +}; + +const STORAGE_URL = process.env.STORAGE_URL; +const STORAGE_REALM_URL = process.env.STORAGE_REALM_URL!; +const STORAGE_SECRET = process.env.STORAGE_SECRET!; + +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("client_id", "ext-api"); + body.append("client_secret", STORAGE_SECRET); + body.append("grant_type", "client_credentials"); + + const res = await fetch(`${STORAGE_REALM_URL}/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: false, + }), + }).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[]) { + 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: false }), + }).catch((e) => console.error(e)); + + if (!res || !res.ok) { + if (res && res.status === HttpStatus.NOT_FOUND) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบแฟ้ม/ไฟล์ในระบบ"); + } + return Boolean(console.error(res ? await res.json() : res)); + } + return (await res.json()) as StorageFile & { uploadUrl: string }; +} + +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 updateFile(path: string[], file: string, metadata: FileProps) { + const res = await fetch(`${STORAGE_URL}/storage/file`, { + method: "PUT", + headers: { + Authorization: `Bearer ${await getToken()}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + from: { path, file }, + ...metadata, + upload: false, + }), + }).catch((e) => console.error(e)); + + if (!res || !res.ok) { + if (res && res.status === HttpStatus.NOT_FOUND) { + throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบไฟล์ในระบบ"); + } + return Boolean(console.error(res ? await res.json() : res)); + } + + return Boolean(res); +} + +export async function deleteFolder(path: string[], name: string) { + const res = await fetch(`${STORAGE_URL}/storage/folder`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${await getToken()}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ path: [...path, name] }), + }).catch((e) => console.error(e)); + + if (!res || !res.ok) { + return Boolean(console.error(res ? await res.json() : res)); + } + return true; +} + +export async function deleteFile(path: string[], file: string) { + const res = await fetch(`${STORAGE_URL}/storage/file`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${await getToken()}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ path, file }), + }).catch((e) => console.error(e)); + + if (!res || !res.ok) { + return Boolean(console.error(res ? await res.json() : res)); + } + return true; +} + +export async function downloadFile(path: string[], file: string) { + 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, "ไม่พบไฟล์ในระบบ"); + } + return Boolean(console.error(res ? await res.json() : res)); + } + return (await res.json()) as StorageFile & { downloadUrl: string }; +}