Merge branch 'develop'
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 8s
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 8s
This commit is contained in:
commit
910dc196c5
9 changed files with 543 additions and 96 deletions
|
|
@ -0,0 +1,16 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Property" (
|
||||
"id" TEXT NOT NULL,
|
||||
"registeredBranchId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"nameEN" TEXT NOT NULL,
|
||||
"type" JSONB NOT NULL,
|
||||
"status" "Status" NOT NULL DEFAULT 'CREATED',
|
||||
"statusOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Property_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Property" ADD CONSTRAINT "Property_registeredBranchId_fkey" FOREIGN KEY ("registeredBranchId") REFERENCES "Branch"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
|
@ -318,6 +318,7 @@ model Branch {
|
|||
workflowTemplate WorkflowTemplate[]
|
||||
taskOrder TaskOrder[]
|
||||
notification Notification[]
|
||||
property Property[]
|
||||
}
|
||||
|
||||
model BranchBank {
|
||||
|
|
@ -1004,6 +1005,23 @@ model Institution {
|
|||
taskOrder TaskOrder[]
|
||||
}
|
||||
|
||||
model Property {
|
||||
id String @id @default(cuid())
|
||||
|
||||
registeredBranch Branch @relation(fields: [registeredBranchId], references: [id])
|
||||
registeredBranchId String
|
||||
|
||||
name String
|
||||
nameEN String
|
||||
|
||||
type Json
|
||||
|
||||
status Status @default(CREATED)
|
||||
statusOrder Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model WorkflowTemplate {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
|
|
|
|||
|
|
@ -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,7 +69,14 @@ const quotationData = (id: string) =>
|
|||
export class DocTemplateController extends Controller {
|
||||
@Get()
|
||||
async getTemplate() {
|
||||
return await listFile(`doc-template/`);
|
||||
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(DOCUMENT_PATH.join("/") + "/");
|
||||
}
|
||||
|
||||
@Get("{documentTemplate}")
|
||||
|
|
@ -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<ArrayBufferLike> | 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,14 +12,16 @@ import {
|
|||
} from "@prisma/client";
|
||||
import { Controller, Get, Query, Request, Route, Security, Tags } from "tsoa";
|
||||
import prisma from "../db";
|
||||
import { createPermCondition } from "../services/permission";
|
||||
import { createPermCondition, createQueryPermissionCondition } from "../services/permission";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import { precisionRound } from "../utils/arithmetic";
|
||||
import dayjs from "dayjs";
|
||||
import { json2csv } from "json-2-csv";
|
||||
import { isSystem } from "../utils/keycloak";
|
||||
import { jsonObjectFrom } from "kysely/helpers/postgres";
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
const permissionQueryCondCompany = createQueryPermissionCondition((_) => true);
|
||||
|
||||
const VAT_DEFAULT = config.vat;
|
||||
|
||||
|
|
@ -645,125 +647,61 @@ export class StatsController extends Controller {
|
|||
@Get("customer-dept")
|
||||
async reportCustomerDept(@Request() req: RequestWithUser) {
|
||||
let query = prisma.$kysely
|
||||
.selectFrom("Quotation")
|
||||
.leftJoin("Invoice", "Quotation.id", "Invoice.quotationId")
|
||||
.selectFrom("Invoice")
|
||||
.leftJoin("Quotation", "Quotation.id", "Invoice.quotationId")
|
||||
.leftJoin("Payment", "Invoice.id", "Payment.invoiceId")
|
||||
.leftJoin("CustomerBranch", "CustomerBranch.id", "Quotation.customerBranchId")
|
||||
.leftJoin("Customer", "Customer.id", "CustomerBranch.customerId")
|
||||
.select([
|
||||
"CustomerBranch.id as customerBranchId",
|
||||
"CustomerBranch.code as customerBranchCode",
|
||||
"CustomerBranch.registerName as customerBranchRegisterName",
|
||||
"CustomerBranch.registerNameEN as customerBranchRegisterNameEN",
|
||||
"CustomerBranch.firstName as customerBranchFirstName",
|
||||
"CustomerBranch.firstNameEN as customerBranchFirstNameEN",
|
||||
"CustomerBranch.lastName as customerBranchFirstName",
|
||||
"CustomerBranch.lastNameEN as customerBranchFirstNameEN",
|
||||
"Customer.customerType",
|
||||
"Quotation.id as quotationId",
|
||||
"Quotation.code as quotationCode",
|
||||
"Quotation.finalPrice as quotationValue",
|
||||
.select((eb) => [
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("CustomerBranch")
|
||||
.select((eb) => [
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("Customer")
|
||||
.selectAll("Customer")
|
||||
.whereRef("Customer.id", "=", "CustomerBranch.customerId"),
|
||||
).as("customer"),
|
||||
])
|
||||
.selectAll("CustomerBranch")
|
||||
.whereRef("CustomerBranch.id", "=", "Quotation.customerBranchId"),
|
||||
).as("customerBranch"),
|
||||
])
|
||||
.select(["Payment.paymentStatus"])
|
||||
.selectAll(["Invoice"])
|
||||
.distinctOn("Quotation.id");
|
||||
.distinctOn("Invoice.id");
|
||||
|
||||
if (!isSystem(req.user)) {
|
||||
query = query.where(({ eb, exists }) =>
|
||||
exists(
|
||||
eb
|
||||
.selectFrom("Branch")
|
||||
.leftJoin("BranchUser", "BranchUser.branchId", "Branch.id")
|
||||
.leftJoin("Branch as SubBranch", "SubBranch.headOfficeId", "Branch.id")
|
||||
.leftJoin("BranchUser as SubBranchUser", "SubBranchUser.branchId", "SubBranch.id")
|
||||
.leftJoin("Branch as HeadBranch", "HeadBranch.id", "Branch.id")
|
||||
.leftJoin("BranchUser as HeadBranchUser", "HeadBranchUser.branchId", "HeadBranch.id")
|
||||
.leftJoin("Branch as SubHeadBranch", "SubHeadBranch.headOfficeId", "HeadBranch.id")
|
||||
.leftJoin(
|
||||
"BranchUser as SubHeadBranchUser",
|
||||
"SubHeadBranchUser.branchId",
|
||||
"SubHeadBranch.id",
|
||||
)
|
||||
.where((eb) => {
|
||||
const cond = [
|
||||
eb("BranchUser.userId", "=", req.user.sub), // NOTE: if user belong to current branch.
|
||||
eb("SubBranchUser.userId", "=", req.user.sub), // NOTE: if user belong to branch under current branch.
|
||||
eb("HeadBranchUser.userId", "=", req.user.sub), // NOTE: if the current branch is under head branch user belong to.
|
||||
eb("SubHeadBranchUser.userId", "=", req.user.sub), // NOTE: if the current branch is under the same head branch user belong to.
|
||||
];
|
||||
return eb.or(cond);
|
||||
})
|
||||
.select("Branch.id"),
|
||||
),
|
||||
);
|
||||
query = query.where(permissionQueryCondCompany(req.user));
|
||||
}
|
||||
|
||||
const ret = await query.execute();
|
||||
const arr = ret.map((item) => {
|
||||
const data: Record<string, any> = {};
|
||||
for (const [key, val] of Object.entries(item)) {
|
||||
if (key.startsWith("customerBranch")) {
|
||||
if (!data["customerBranch"]) data["customerBranch"] = {};
|
||||
data["customerBranch"][key.slice(14).slice(0, 1).toLowerCase() + key.slice(14).slice(1)] =
|
||||
val;
|
||||
} else if (key.startsWith("customerType")) {
|
||||
data["customerBranch"]["customer"] = { customerType: val };
|
||||
} else {
|
||||
data[key as keyof typeof data] = val;
|
||||
}
|
||||
}
|
||||
return data as Invoice & {
|
||||
quotationId: string;
|
||||
quotationCode: string;
|
||||
quotationValue: number;
|
||||
paymentStatus: PaymentStatus;
|
||||
customerBranch: CustomerBranch & { customer: { customerType: CustomerType } };
|
||||
};
|
||||
});
|
||||
|
||||
return arr
|
||||
return ret
|
||||
.reduce<
|
||||
{
|
||||
paid: number;
|
||||
unpaid: number;
|
||||
customerBranch: CustomerBranch & { customer: { customerType: CustomerType } };
|
||||
_quotation: { id: string; code: string; value: number }[];
|
||||
customerBranch: CustomerBranch & { customer: Customer };
|
||||
}[]
|
||||
>((acc, item) => {
|
||||
const exists = acc.find((v) => v.customerBranch.id === item.customerBranch.id);
|
||||
const exists = acc.find((v) => v.customerBranch.id === item.customerBranch!.id);
|
||||
|
||||
const quotation = {
|
||||
id: item.quotationId,
|
||||
code: item.quotationCode,
|
||||
value:
|
||||
item.quotationValue -
|
||||
(item.paymentStatus === "PaymentSuccess" && item.amount ? item.amount : 0),
|
||||
};
|
||||
if (!item.amount) return acc;
|
||||
|
||||
if (!exists) {
|
||||
return acc.concat({
|
||||
_quotation: [quotation],
|
||||
customerBranch: item.customerBranch,
|
||||
paid: item.paymentStatus === "PaymentSuccess" && item.amount ? item.amount : 0,
|
||||
unpaid: quotation.value,
|
||||
customerBranch: item.customerBranch as CustomerBranch & { customer: Customer },
|
||||
paid: item.paymentStatus === "PaymentSuccess" ? item.amount : 0,
|
||||
unpaid: item.paymentStatus !== "PaymentSuccess" ? item.amount : 0,
|
||||
});
|
||||
} else {
|
||||
exists[item.paymentStatus === "PaymentSuccess" ? "paid" : "unpaid"] += item.amount;
|
||||
}
|
||||
|
||||
const same = exists._quotation.find((v) => v.id === item.quotationId);
|
||||
|
||||
if (item.paymentStatus === "PaymentSuccess" && item.amount) {
|
||||
exists.paid += item.amount;
|
||||
if (same) same.value -= item.amount;
|
||||
}
|
||||
|
||||
if (!same) exists._quotation.push(quotation);
|
||||
|
||||
exists.unpaid = exists._quotation.reduce((a, c) => a + c.value, 0);
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.map((v) => {
|
||||
return { ...v, _quotation: undefined };
|
||||
});
|
||||
.map((v) => ({ ...v, _quotation: undefined }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
187
src/controllers/04-properties-controller.ts
Normal file
187
src/controllers/04-properties-controller.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Path,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Request,
|
||||
Route,
|
||||
Security,
|
||||
Tags,
|
||||
} from "tsoa";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import prisma from "../db";
|
||||
import { Prisma, Status } from "@prisma/client";
|
||||
import {
|
||||
branchRelationPermInclude,
|
||||
createPermCheck,
|
||||
createPermCondition,
|
||||
} from "../services/permission";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
import { notFoundError } from "../utils/error";
|
||||
import { filterStatus } from "../services/prisma";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
|
||||
type PropertyPayload = {
|
||||
name: string;
|
||||
nameEN: string;
|
||||
type: Record<string, any>;
|
||||
registeredBranchId?: string;
|
||||
status?: Status;
|
||||
};
|
||||
|
||||
const permissionCondCompany = createPermCondition((_) => true);
|
||||
const permissionCheckCompany = createPermCheck((_) => true);
|
||||
|
||||
@Route("api/v1/property")
|
||||
@Tags("Property")
|
||||
@Security("keycloak")
|
||||
export class PropertiesController extends Controller {
|
||||
@Get()
|
||||
async getProperties(
|
||||
@Request() req: RequestWithUser,
|
||||
@Query() page: number = 1,
|
||||
@Query() pageSize: number = 30,
|
||||
@Query() status?: Status,
|
||||
@Query() query = "",
|
||||
@Query() activeOnly?: boolean,
|
||||
) {
|
||||
const where = {
|
||||
OR: queryOrNot(query, [{ name: { contains: query } }, { nameEN: { contains: query } }]),
|
||||
AND: {
|
||||
...filterStatus(activeOnly ? Status.ACTIVE : status),
|
||||
registeredBranch: {
|
||||
OR: permissionCondCompany(req.user, { activeOnly: true }),
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.PropertyWhereInput;
|
||||
const [result, total] = await prisma.$transaction([
|
||||
prisma.property.findMany({
|
||||
where,
|
||||
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
|
||||
take: pageSize,
|
||||
skip: (page - 1) * pageSize,
|
||||
}),
|
||||
prisma.property.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
result,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("{propertyId}")
|
||||
async getPropertyById(@Request() _req: RequestWithUser, @Path() propertyId: string) {
|
||||
const record = await prisma.property.findFirst({
|
||||
where: { id: propertyId },
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
|
||||
if (!record) throw notFoundError("Property");
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async createProperty(@Request() req: RequestWithUser, @Body() body: PropertyPayload) {
|
||||
const where = {
|
||||
OR: [{ name: { contains: body.name } }, { nameEN: { contains: body.nameEN } }],
|
||||
AND: {
|
||||
registeredBranch: {
|
||||
OR: permissionCondCompany(req.user),
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.PropertyWhereInput;
|
||||
|
||||
const exists = await prisma.property.findFirst({ where });
|
||||
|
||||
if (exists) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Property with this name already exists",
|
||||
"samePropertyNameExists",
|
||||
);
|
||||
}
|
||||
|
||||
const userAffiliatedBranch = await prisma.branch.findFirst({
|
||||
include: branchRelationPermInclude(req.user),
|
||||
where: body.registeredBranchId
|
||||
? { id: body.registeredBranchId }
|
||||
: {
|
||||
user: { some: { userId: req.user.sub } },
|
||||
},
|
||||
});
|
||||
if (!userAffiliatedBranch) {
|
||||
throw new HttpError(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"You must be affilated with at least one branch or specify branch to be registered (System permission required).",
|
||||
"reqMinAffilatedBranch",
|
||||
);
|
||||
}
|
||||
|
||||
await permissionCheckCompany(req.user, userAffiliatedBranch);
|
||||
|
||||
return await prisma.property.create({
|
||||
data: {
|
||||
...body,
|
||||
statusOrder: +(body.status === "INACTIVE"),
|
||||
registeredBranchId: userAffiliatedBranch.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Put("{propertyId}")
|
||||
async updatePropertyById(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() propertyId: string,
|
||||
@Body() body: PropertyPayload,
|
||||
) {
|
||||
const record = await prisma.property.findUnique({
|
||||
where: { id: propertyId },
|
||||
include: {
|
||||
registeredBranch: {
|
||||
include: branchRelationPermInclude(req.user),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!record) throw notFoundError("Property");
|
||||
|
||||
await permissionCheckCompany(req.user, record.registeredBranch);
|
||||
|
||||
return await prisma.property.update({
|
||||
where: { id: propertyId },
|
||||
data: {
|
||||
...body,
|
||||
statusOrder: +(body.status === "INACTIVE"),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Delete("{propertyId}")
|
||||
async deletePropertyById(@Request() req: RequestWithUser, @Path() propertyId: string) {
|
||||
const record = await prisma.property.findUnique({
|
||||
where: { id: propertyId },
|
||||
include: {
|
||||
registeredBranch: {
|
||||
include: branchRelationPermInclude(req.user),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!record) throw notFoundError("Property");
|
||||
|
||||
await permissionCheckCompany(req.user, record.registeredBranch);
|
||||
|
||||
return await prisma.property.delete({
|
||||
where: { id: propertyId },
|
||||
});
|
||||
}
|
||||
}
|
||||
39
src/interfaces/edm.ts
Normal file
39
src/interfaces/edm.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
|
||||
path: string;
|
||||
upload: boolean;
|
||||
|
||||
updatedAt: string | Date;
|
||||
updatedBy: string;
|
||||
createdAt: string | Date;
|
||||
createdBy: string;
|
||||
}
|
||||
170
src/services/edm/edm-api.ts
Normal file
170
src/services/edm/edm-api.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
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<StorageFile, "title" | "description" | "author" | "keyword" | "category">
|
||||
> & {
|
||||
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<false | (StorageFile & { uploadUrl: 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: true }),
|
||||
}).catch((e) => console.error(e));
|
||||
|
||||
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<false | (StorageFile & { downloadUrl: 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, "ไม่พบไฟล์ในระบบ");
|
||||
}
|
||||
console.error(res ? await res.json() : res);
|
||||
return false;
|
||||
}
|
||||
return await res.json();
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import HttpError from "../interfaces/http-error";
|
|||
import HttpStatus from "../interfaces/http-status";
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
import { isSystem } from "../utils/keycloak";
|
||||
import { ExpressionBuilder } from "kysely";
|
||||
import { DB } from "../generated/kysely/types";
|
||||
|
||||
export function branchRelationPermInclude(user: RequestWithUser["user"]) {
|
||||
return {
|
||||
|
|
@ -133,3 +135,47 @@ export function createPermCheck(globalAllow: (user: RequestWithUser["user"]) =>
|
|||
return branch;
|
||||
};
|
||||
}
|
||||
|
||||
export function createQueryPermissionCondition(
|
||||
globalAllow: (user: RequestWithUser["user"]) => boolean,
|
||||
opts?: { alwaysIncludeHead?: boolean },
|
||||
) {
|
||||
return (user: RequestWithUser["user"]) =>
|
||||
({ eb, exists }: ExpressionBuilder<DB, keyof DB>) =>
|
||||
exists(
|
||||
eb
|
||||
.selectFrom("Branch")
|
||||
.leftJoin("BranchUser", "BranchUser.branchId", "Branch.id")
|
||||
.leftJoin("Branch as SubBranch", "SubBranch.headOfficeId", "Branch.id")
|
||||
.leftJoin("BranchUser as SubBranchUser", "SubBranchUser.branchId", "SubBranch.id")
|
||||
.leftJoin("Branch as HeadBranch", "HeadBranch.id", "Branch.id")
|
||||
.leftJoin("BranchUser as HeadBranchUser", "HeadBranchUser.branchId", "HeadBranch.id")
|
||||
.leftJoin("Branch as SubHeadBranch", "SubHeadBranch.headOfficeId", "HeadBranch.id")
|
||||
.leftJoin(
|
||||
"BranchUser as SubHeadBranchUser",
|
||||
"SubHeadBranchUser.branchId",
|
||||
"SubHeadBranch.id",
|
||||
)
|
||||
.where((eb) => {
|
||||
const cond = [
|
||||
eb("BranchUser.userId", "=", user.sub), // NOTE: if user belong to current branch.
|
||||
];
|
||||
|
||||
if (globalAllow?.(user) || opts?.alwaysIncludeHead) {
|
||||
cond.push(
|
||||
eb("SubBranchUser.userId", "=", user.sub), // NOTE: if user belong to branch under current branch.
|
||||
);
|
||||
}
|
||||
|
||||
if (globalAllow(user)) {
|
||||
cond.push(
|
||||
eb("HeadBranchUser.userId", "=", user.sub), // NOTE: if the current branch is under head branch user belong to.
|
||||
eb("SubHeadBranchUser.userId", "=", user.sub), // NOTE: if the current branch is under the same head branch user belong to.
|
||||
);
|
||||
}
|
||||
|
||||
return eb.or(cond);
|
||||
})
|
||||
.select("Branch.id"),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
{ "name": "Employee Other Info" },
|
||||
{ "name": "Institution" },
|
||||
{ "name": "Workflow" },
|
||||
{ "name": "Property" },
|
||||
{ "name": "Product Group" },
|
||||
{ "name": "Product" },
|
||||
{ "name": "Work" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue